├── .travis.yml ├── LICENSE ├── README.md ├── errors.go ├── format.go ├── format_test.go ├── parser.go ├── parser_internal_test.go ├── parser_test.go ├── scanner.go ├── scanner_test.go ├── statement.go └── token.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | 6 | before_install: 7 | - go get -t -v ./... 8 | 9 | script: 10 | - go test -race -coverprofile=coverage.txt -covermode=atomic 11 | 12 | after_success: 13 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Hervé GOUCHET 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Awql Parser 2 | 3 | [![GoDoc](https://godoc.org/github.com/rvflash/awql-parser?status.svg)](https://godoc.org/github.com/rvflash/awql-parser) 4 | [![Build Status](https://img.shields.io/travis/rvflash/awql-parser.svg)](https://travis-ci.org/rvflash/awql-parser) 5 | [![Code Coverage](https://img.shields.io/codecov/c/github/rvflash/awql-parser.svg)](http://codecov.io/github/rvflash/awql-parser?branch=master) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/rvflash/awql-parser)](https://goreportcard.com/report/github.com/rvflash/awql-parser) 7 | 8 | 9 | Parser for parsing AWQL SELECT, DESCRIBE, SHOW and CREATE VIEW statements. 10 | 11 | Only the first statement is supported by Adwords API, the others are proposed by the AWQL command line tool. 12 | 13 | ## Examples 14 | 15 | ### Unknown single statement. 16 | 17 | ```go 18 | q := `SELECT CampaignId, CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT;` 19 | stmt, _ := awql.NewParser(strings.NewReader(q)).ParseRow() 20 | if stmt, ok := stmt.(awql.SelectStmt); ok { 21 | fmt.Println(stmt.SourceName()) 22 | // Output: CAMPAIGN_PERFORMANCE_REPORT 23 | } 24 | ``` 25 | 26 | ### Select statement. 27 | 28 | ```go 29 | q := `SELECT AdGroupName FROM ADGROUP_PERFORMANCE_REPORT;` 30 | stmt, _ := awql.NewParser(strings.NewReader(q)).ParseSelect() 31 | fmt.Printf("Gets the column named %v from %v.\n", stmt.Columns()[0].Name(), stmt.SourceName()) 32 | // Output: Gets the column named AdGroupName from ADGROUP_PERFORMANCE_REPORT. 33 | ``` 34 | 35 | ### Multiple statements. 36 | 37 | ```go 38 | q := `SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT ORDER BY 1 LIMIT 5\GDESC ADGROUP_PERFORMANCE_REPORT AdGroupName;` 39 | stmts, _ := awql.NewParser(strings.NewReader(q)).Parse() 40 | for _, stmt := range stmts { 41 | switch stmt.(type) { 42 | case awql.SelectStmt: 43 | fmt.Println(stmt.(awql.SelectStmt).OrderList()[0].Name()) 44 | case awql.DescribeStmt: 45 | fmt.Println(stmt.(awql.DescribeStmt).SourceName()) 46 | fmt.Println(stmt.(awql.DescribeStmt).Columns()[0].Name()) 47 | } 48 | } 49 | // Output: 50 | // CampaignName 51 | // ADGROUP_PERFORMANCE_REPORT 52 | // AdGroupName 53 | ``` -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package awqlparse 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // ParserError represents an error of parse. 9 | type ParserError struct { 10 | s string 11 | a interface{} 12 | } 13 | 14 | // NewParserError returns an error with the parsing. 15 | func NewParserError(text string) error { 16 | return &ParserError{s: formatError(text)} 17 | } 18 | 19 | // NewXParserError returns an error with the parsing with more information about it. 20 | func NewXParserError(text string, arg interface{}) error { 21 | return &ParserError{s: formatError(text), a: arg} 22 | } 23 | 24 | // Error returns the message of the parse error. 25 | func (e *ParserError) Error() string { 26 | if e.a != nil { 27 | return fmt.Sprintf("ParserError.%v (%v)", e.s, e.a) 28 | } 29 | return fmt.Sprintf("ParserError.%v", e.s) 30 | } 31 | 32 | // formatError returns a string in upper case with underscore instead of space. 33 | // As the Adwords API outputs its errors. 34 | func formatError(s string) string { 35 | return strings.Replace(strings.ToUpper(strings.TrimSpace(s)), " ", "_", -1) 36 | } 37 | -------------------------------------------------------------------------------- /format.go: -------------------------------------------------------------------------------- 1 | package awqlparse 2 | 3 | import "strconv" 4 | 5 | // String outputs a create view statement. 6 | func (s CreateViewStatement) String() (q string) { 7 | if s.SourceName() == "" { 8 | return 9 | } 10 | q = "CREATE " 11 | if s.ReplaceMode() { 12 | q += "OR REPLACE " 13 | } 14 | q += "VIEW " + s.SourceName() 15 | 16 | // Concatenates field names. 17 | cols := s.Columns() 18 | if size := len(cols); size > 0 { 19 | q += " (" 20 | for i, c := range cols { 21 | if i > 0 { 22 | q += ", " 23 | } 24 | q += c.Name() 25 | } 26 | q += ")" 27 | } 28 | 29 | // Adds the data source. 30 | v := s.View.String() 31 | if v == "" { 32 | return "" 33 | } 34 | q += " AS " + v 35 | 36 | return 37 | } 38 | 39 | // String outputs a describe statement. 40 | func (s DescribeStatement) String() (q string) { 41 | if s.SourceName() == "" { 42 | return 43 | } 44 | q = "DESC " 45 | if s.FullMode() { 46 | q += "FULL " 47 | } 48 | q += s.SourceName() 49 | 50 | cols := s.Columns() 51 | if len(cols) == 1 { 52 | q += " " + cols[0].Name() 53 | } 54 | 55 | return 56 | } 57 | 58 | // String outputs a select statement. 59 | func (s SelectStatement) String() (q string) { 60 | if len(s.Columns()) == 0 || s.SourceName() == "" { 61 | return 62 | } 63 | q = "SELECT " 64 | 65 | // Adds columns. 66 | for i, c := range s.Columns() { 67 | if i > 0 { 68 | q += ", " 69 | } 70 | // Distinct value. 71 | var s string 72 | if c.Distinct() { 73 | s = "DISTINCT " 74 | } 75 | s += c.Name() 76 | // Method name. 77 | if method, ok := c.UseFunction(); ok { 78 | s = method + "(" + s + ")" 79 | } 80 | // Alias. 81 | if c.Alias() != "" { 82 | s += " AS " + c.Alias() 83 | } 84 | q += s 85 | } 86 | 87 | // Adds data source name. 88 | q += " FROM " + s.SourceName() 89 | q += s.whereString() 90 | q += s.duringString() 91 | 92 | // Adds group by clause. 93 | g := s.GroupList() 94 | if gs := len(g); gs > 0 { 95 | q += " GROUP BY " 96 | for i := 0; i < gs; i++ { 97 | if i > 0 { 98 | q += ", " 99 | } 100 | q += strconv.Itoa(g[i].Position()) 101 | } 102 | } 103 | 104 | // Adds sort orders. 105 | o := s.OrderList() 106 | if os := len(o); os > 0 { 107 | q += " ORDER BY " 108 | for i := 0; i < os; i++ { 109 | if i > 0 { 110 | q += ", " 111 | } 112 | q += strconv.Itoa(o[i].Position()) 113 | if o[i].SortDescending() { 114 | q += " DESC" 115 | } 116 | } 117 | } 118 | 119 | // Adds limit clause. 120 | if rc, ok := s.PageSize(); ok { 121 | q += " LIMIT " 122 | if si := s.StartIndex(); si > 0 { 123 | q += strconv.Itoa(si) + ", " 124 | } 125 | q += strconv.Itoa(rc) 126 | } 127 | 128 | return 129 | } 130 | 131 | // LegacyString outputs a select statement as expected by Google Adwords. 132 | // Indeed, aggregate functions, ORDER BY, GROUP BY and LIMIT are not supported for reports. 133 | func (s SelectStatement) LegacyString() (q string) { 134 | if len(s.Columns()) == 0 || s.SourceName() == "" { 135 | return 136 | } 137 | q = "SELECT " 138 | 139 | // Concatenates selected fields. 140 | for i, c := range s.Columns() { 141 | if i > 0 { 142 | q += ", " 143 | } 144 | q += c.Name() 145 | } 146 | 147 | // Adds data source name. 148 | q += " FROM " + s.SourceName() 149 | q += s.whereString() 150 | q += s.duringString() 151 | 152 | return 153 | } 154 | 155 | // duringString outputs a where clause. 156 | func (s SelectStatement) whereString() (q string) { 157 | if len(s.ConditionList()) > 0 { 158 | q += " WHERE " 159 | for i, c := range s.ConditionList() { 160 | if i > 0 { 161 | q += " AND " 162 | } 163 | q += c.Name() + " " + c.Operator() 164 | val, lit := c.Value() 165 | if len(val) > 1 { 166 | q += " [" 167 | for y, v := range val { 168 | if y > 0 { 169 | q += " ," 170 | } 171 | if lit { 172 | q += " " + v 173 | } else { 174 | q += " " + strconv.Quote(v) 175 | } 176 | } 177 | q += " ]" 178 | } else if lit { 179 | q += " " + val[0] 180 | } else { 181 | q += " " + strconv.Quote(val[0]) 182 | } 183 | } 184 | } 185 | 186 | return 187 | } 188 | 189 | // duringString outputs a during clause. 190 | func (s SelectStatement) duringString() (q string) { 191 | d := s.DuringList() 192 | if ds := len(d); ds > 0 { 193 | q += " DURING " 194 | if ds == 2 { 195 | q += d[0] + "," + d[1] 196 | } else { 197 | // Literal range date 198 | q += d[0] 199 | } 200 | } 201 | 202 | return 203 | } 204 | 205 | // String outputs a show statement. 206 | func (s ShowStatement) String() (q string) { 207 | q = "SHOW " 208 | if s.FullMode() { 209 | q += "FULL " 210 | } 211 | q += "TABLES" 212 | 213 | if p, used := s.LikePattern(); used { 214 | var str string 215 | switch { 216 | case p.Equal != "": 217 | str = p.Equal 218 | case p.Contains != "": 219 | str = "%" + p.Contains + "%" 220 | case p.Prefix != "": 221 | str = p.Prefix + "%" 222 | case p.Suffix != "": 223 | str = "%" + p.Suffix 224 | } 225 | q += " LIKE " + strconv.Quote(str) 226 | } 227 | 228 | if str, used := s.WithFieldName(); used { 229 | q += " WITH " + strconv.Quote(str) 230 | } 231 | 232 | return 233 | } 234 | -------------------------------------------------------------------------------- /format_test.go: -------------------------------------------------------------------------------- 1 | package awqlparse_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | awql "github.com/rvflash/awql-parser" 8 | ) 9 | 10 | func TestSelectStmt_String(t *testing.T) { 11 | var tests = []struct { 12 | fq, tq string 13 | }{ 14 | { 15 | fq: `DESC FULL CAMPAIGN_PERFORMANCE_REPORT`, 16 | }, 17 | { 18 | fq: `DESC CAMPAIGN_PERFORMANCE_REPORT CampaignStatus`, 19 | }, 20 | { 21 | fq: `SHOW FULL TABLES`, 22 | }, 23 | { 24 | fq: `SHOW FULL TABLES LIKE "%rv"`, 25 | }, 26 | { 27 | fq: `SHOW FULL TABLES LIKE "%rv%"`, 28 | }, 29 | { 30 | fq: `SHOW FULL TABLES LIKE "rv%"`, 31 | }, 32 | { 33 | fq: `SHOW TABLES LIKE "rv"`, 34 | }, 35 | { 36 | fq: `SHOW TABLES WITH "rv"`, 37 | }, 38 | { 39 | fq: `CREATE VIEW rv AS SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT LIMIT 10`, 40 | }, 41 | { 42 | fq: `CREATE VIEW rv (Name, Cost) AS SELECT CampaignName, Cost FROM CAMPAIGN_PERFORMANCE_REPORT DURING TODAY`, 43 | }, 44 | { 45 | fq: `CREATE OR REPLACE VIEW rv AS SELECT CampaignId, Cost FROM CAMPAIGN_PERFORMANCE_REPORT DURING TODAY`, 46 | }, 47 | { 48 | fq: `SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT`, 49 | }, 50 | { 51 | fq: `SELECT SUM(Cost) AS c FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignStatus = "ENABLED"`, 52 | tq: `SELECT Cost FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignStatus = "ENABLED"`, 53 | }, 54 | { 55 | fq: `SELECT CampaignName, Cost FROM CAMPAIGN_PERFORMANCE_REPORT GROUP BY 1 ORDER BY 2 DESC`, 56 | tq: `SELECT CampaignName, Cost FROM CAMPAIGN_PERFORMANCE_REPORT`, 57 | }, 58 | { 59 | fq: `SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT DURING 20161224,20161225 LIMIT 10`, 60 | tq: `SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT DURING 20161224,20161225`, 61 | }, 62 | } 63 | 64 | for i, qt := range tests { 65 | stmt, err := awql.NewParser(strings.NewReader(qt.fq)).ParseRow() 66 | if err != nil { 67 | t.Fatalf("%d. Expected no error with '%v', received %v", i, qt.fq, err) 68 | } 69 | // Use original query as expected query. 70 | if qt.tq == "" { 71 | qt.tq = qt.fq 72 | } 73 | // Checks the legacy stringer. 74 | if sStmt, ok := stmt.(awql.SelectStmt); ok { 75 | if q := sStmt.LegacyString(); q != qt.tq { 76 | t.Errorf("%d. Expected the legacy query '%v' with '%s', received '%v'", i, qt.tq, qt.fq, q) 77 | } 78 | } 79 | // Checks the default stringer. 80 | if q := stmt.String(); q != qt.fq { 81 | t.Errorf("%d. Expected the query '%v' with '%s', received '%v'", i, qt.fq, qt.fq, q) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package awqlparse 2 | 3 | import ( 4 | "io" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Like with % 10 | const wildcard = "%" 11 | 12 | // Parser represents a parser. 13 | type Parser struct { 14 | s *Scanner 15 | buf struct { 16 | t Token // last read token 17 | l string // last read literal 18 | n int // buffer size, char by char, maximum value: 1 19 | } 20 | } 21 | 22 | // Error messages. 23 | var ( 24 | ErrMsgBadStmt = "unkwown statement" 25 | ErrMsgMissingSrc = "missing source" 26 | ErrMsgColumnsNotMatch = "invalid method" 27 | ErrMsgBadColumn = "invalid method" 28 | ErrMsgBadMethod = "invalid method" 29 | ErrMsgBadField = "invalid field" 30 | ErrMsgBadFunc = "invalid function" 31 | ErrMsgBadSrc = "invalid source" 32 | ErrMsgBadDuring = "invalid during" 33 | ErrMsgBadGroup = "invalid group by" 34 | ErrMsgBadOrder = "invalid order by" 35 | ErrMsgBadLimit = "invalid limit" 36 | ErrMsgSyntax = "syntax near" 37 | ErrMsgDuringSize = "unexpected number of date range" 38 | ErrMsgDuringLitSize = "expected date range literal" 39 | ErrMsgDuringDateSize = "expected no literal date" 40 | ) 41 | 42 | // NewParser returns a new instance of Parser. 43 | func NewParser(r io.Reader) *Parser { 44 | return &Parser{s: NewScanner(r)} 45 | } 46 | 47 | // Parse parses a AWQL statement. 48 | func (p *Parser) Parse() (statements []Stmt, err error) { 49 | for { 50 | var stmt Stmt 51 | // Retrieve the first token of the statement. 52 | tk, _ := p.scanIgnoreWhitespace() 53 | switch tk { 54 | case DESC, DESCRIBE: 55 | p.unscan() 56 | stmt, err = p.ParseDescribe() 57 | case CREATE: 58 | p.unscan() 59 | stmt, err = p.ParseCreateView() 60 | case SELECT: 61 | p.unscan() 62 | stmt, err = p.ParseSelect() 63 | case SHOW: 64 | p.unscan() 65 | stmt, err = p.ParseShow() 66 | default: 67 | err = NewParserError(ErrMsgBadStmt) 68 | } 69 | if err != nil { 70 | return 71 | } 72 | statements = append(statements, stmt) 73 | 74 | // If the next token is EOF, break the loop. 75 | if tk, _ := p.scanIgnoreWhitespace(); tk == EOF { 76 | break 77 | } else { 78 | p.unscan() 79 | } 80 | } 81 | return 82 | } 83 | 84 | // ParseRow parses a AWQL statement and returns only the first. 85 | func (p *Parser) ParseRow() (Stmt, error) { 86 | stmts, err := p.Parse() 87 | if err != nil { 88 | return nil, err 89 | } 90 | return stmts[0], nil 91 | } 92 | 93 | // ParseDescribe parses a AWQL DESCRIBE statement. 94 | func (p *Parser) ParseDescribe() (DescribeStmt, error) { 95 | // First token should be a "DESC" keyword. 96 | if tk, literal := p.scanIgnoreWhitespace(); tk != DESC && tk != DESCRIBE { 97 | return nil, NewXParserError(ErrMsgBadMethod, literal) 98 | } 99 | stmt := &DescribeStatement{} 100 | 101 | // Next we may see the "FULL" keyword. 102 | if tk, _ := p.scanIgnoreWhitespace(); tk == FULL { 103 | stmt.Full = true 104 | } else { 105 | p.unscan() 106 | } 107 | 108 | // Next we should read the table name. 109 | if tk, literal := p.scanIgnoreWhitespace(); tk == IDENTIFIER { 110 | stmt.TableName = literal 111 | } else { 112 | return nil, NewXParserError(ErrMsgBadSrc, literal) 113 | } 114 | 115 | // Next we may see a column name. 116 | if tk, literal := p.scanIgnoreWhitespace(); tk == IDENTIFIER { 117 | field := NewDynamicColumn(NewColumn(literal, ""), "", false) 118 | stmt.Fields = append(stmt.Fields, field) 119 | } else { 120 | p.unscan() 121 | } 122 | 123 | // Finally, we should find the end of the query. 124 | var err error 125 | if stmt.GModifier, err = p.scanQueryEnding(); err != nil { 126 | return nil, err 127 | } 128 | return stmt, nil 129 | } 130 | 131 | // ParseCreateView parses a AWQL CREATE VIEW statement. 132 | func (p *Parser) ParseCreateView() (CreateViewStmt, error) { 133 | // First token should be a "CREATE" keyword. 134 | if tk, literal := p.scanIgnoreWhitespace(); tk != CREATE { 135 | return nil, NewXParserError(ErrMsgBadMethod, literal) 136 | } 137 | stmt := &CreateViewStatement{} 138 | 139 | // Next we may see the "OR" keyword. 140 | if tk, _ := p.scanIgnoreWhitespace(); tk == OR { 141 | if tk, literal := p.scanIgnoreWhitespace(); tk != REPLACE { 142 | return nil, NewXParserError(ErrMsgSyntax, literal) 143 | } 144 | stmt.Replace = true 145 | } else { 146 | p.unscan() 147 | } 148 | 149 | // Next we should see the "VIEW" keyword. 150 | if tk, literal := p.scanIgnoreWhitespace(); tk != VIEW { 151 | return nil, NewXParserError(ErrMsgSyntax, literal) 152 | } 153 | 154 | // Next we should read the view name. 155 | tk, literal := p.scanIgnoreWhitespace() 156 | if tk != IDENTIFIER { 157 | return nil, NewXParserError(ErrMsgBadSrc, literal) 158 | } 159 | stmt.TableName = literal 160 | 161 | // Next we may see columns names. 162 | if tk, _ := p.scanIgnoreWhitespace(); tk == LEFT_PARENTHESIS { 163 | for { 164 | if tk, literal := p.scanIgnoreWhitespace(); tk == RIGHT_PARENTHESIS { 165 | break 166 | } else if tk == IDENTIFIER { 167 | stmt.Fields = append(stmt.Fields, NewDynamicColumn(NewColumn(literal, ""), "", false)) 168 | } else if tk == COMMA { 169 | // If the next token is not an "COMMA" then break the loop. 170 | continue 171 | } else { 172 | return nil, NewXParserError(ErrMsgBadField, literal) 173 | } 174 | } 175 | } else { 176 | p.unscan() 177 | } 178 | 179 | // Next we should see the "AS" keyword. 180 | if tk, literal := p.scanIgnoreWhitespace(); tk != AS { 181 | return nil, NewXParserError(ErrMsgSyntax, literal) 182 | } 183 | 184 | // And finally, the query source of the view. 185 | selectStmt, err := p.ParseSelect() 186 | if err != nil { 187 | return nil, err 188 | } 189 | stmt.View = selectStmt.(*SelectStatement) 190 | 191 | // Checks if the nomber of view's columns match with the source. 192 | if vcs := len(stmt.Fields); vcs > 0 { 193 | if vcs != len(stmt.View.Fields) { 194 | return nil, NewParserError(ErrMsgColumnsNotMatch) 195 | } 196 | } 197 | return stmt, nil 198 | } 199 | 200 | // ParseShow parses a AWQL SHOW statement. 201 | func (p *Parser) ParseShow() (ShowStmt, error) { 202 | // First token should be a "SHOW" keyword. 203 | if tk, literal := p.scanIgnoreWhitespace(); tk != SHOW { 204 | return nil, NewXParserError(ErrMsgBadMethod, literal) 205 | } 206 | stmt := &ShowStatement{} 207 | 208 | // Next we may see the "FULL" keyword. 209 | if tk, _ := p.scanIgnoreWhitespace(); tk == FULL { 210 | stmt.Full = true 211 | } else { 212 | p.unscan() 213 | } 214 | 215 | // Next we should see the "TABLES" keyword. 216 | if tk, literal := p.scanIgnoreWhitespace(); tk != TABLES { 217 | return nil, NewXParserError(ErrMsgSyntax, literal) 218 | } 219 | 220 | // Next we may find a LIKE or WITH keyword. 221 | if clause, _ := p.scanIgnoreWhitespace(); clause == LIKE || clause == WITH { 222 | // And then, the search pattern. 223 | tk, pattern := p.scanIgnoreWhitespace() 224 | switch tk { 225 | case IDENTIFIER: 226 | if clause == LIKE { 227 | return nil, NewXParserError(ErrMsgSyntax, pattern) 228 | } 229 | stmt.With = pattern 230 | stmt.UseWith = true 231 | case STRING: 232 | if clause == LIKE { 233 | // Like clause can have a wildcard characters in the pattern. 234 | wl := strings.HasPrefix(pattern, wildcard) 235 | wr := strings.HasSuffix(pattern, wildcard) 236 | like := Pattern{} 237 | if wl == wr && wl { 238 | like.Contains = strings.Trim(pattern, wildcard) 239 | } else if wl == wr && !wl { 240 | like.Equal = pattern 241 | } else if wl { 242 | like.Suffix = strings.TrimPrefix(pattern, wildcard) 243 | } else if wr { 244 | like.Prefix = strings.TrimSuffix(pattern, wildcard) 245 | } 246 | stmt.Like = like 247 | } else { 248 | stmt.With = pattern 249 | stmt.UseWith = true 250 | } 251 | default: 252 | return nil, NewXParserError(ErrMsgSyntax, pattern) 253 | } 254 | } else { 255 | p.unscan() 256 | } 257 | 258 | // Finally, we should find the end of the query. 259 | var err error 260 | if stmt.GModifier, err = p.scanQueryEnding(); err != nil { 261 | return nil, err 262 | } 263 | return stmt, nil 264 | } 265 | 266 | // ParseSelect parses a AWQL SELECT statement. 267 | func (p *Parser) ParseSelect() (SelectStmt, error) { 268 | // First token should be a "SELECT" keyword. 269 | if tk, literal := p.scanIgnoreWhitespace(); tk != SELECT { 270 | return nil, NewXParserError(ErrMsgBadMethod, literal) 271 | } 272 | stmt := &SelectStatement{} 273 | 274 | // Next we should loop over all our comma-delimited fields. 275 | for { 276 | // Read a field. 277 | field := &DynamicColumn{Column: &Column{}} 278 | tk, literal := p.scanIgnoreWhitespace() 279 | switch tk { 280 | case ASTERISK: 281 | field.ColumnName = literal 282 | case DISTINCT: 283 | if err := p.scanDistinct(field); err != nil { 284 | return nil, err 285 | } 286 | case IDENTIFIER: 287 | // Next we may find a function declaration. 288 | if tk, _ := p.scan(); tk != LEFT_PARENTHESIS { 289 | // Just a column name. 290 | field.ColumnName = literal 291 | p.unscan() 292 | } else if !isFunction(literal) { 293 | // This function does not exist. 294 | return nil, NewXParserError(ErrMsgBadFunc, literal) 295 | } else { 296 | // It is an aggregate function. 297 | field.Method = strings.ToUpper(literal) 298 | 299 | // Next we may read a distinct clause, a column position or just a column name. 300 | tk, literal = p.scanIgnoreWhitespace() 301 | switch tk { 302 | case ASTERISK: 303 | // Accept the rune '*' only with the count function. 304 | if field.Method != "COUNT" { 305 | return nil, NewXParserError(ErrMsgSyntax, literal) 306 | } 307 | field.ColumnName = literal 308 | case DISTINCT: 309 | if err := p.scanDistinct(field); err != nil { 310 | return nil, err 311 | } 312 | case DIGIT: 313 | digit, _ := strconv.Atoi(literal) 314 | column, err := stmt.searchColumnByPosition(digit) 315 | if err != nil { 316 | return nil, NewXParserError(ErrMsgSyntax, literal) 317 | } 318 | field.Column = column.Column 319 | case IDENTIFIER: 320 | field.ColumnName = literal 321 | default: 322 | return nil, NewXParserError(ErrMsgBadFunc, literal) 323 | } 324 | 325 | // Next, we expect the end of the function. 326 | if tk, _ := p.scanIgnoreWhitespace(); tk != RIGHT_PARENTHESIS { 327 | return nil, NewXParserError(ErrMsgBadFunc, literal) 328 | } 329 | } 330 | default: 331 | return nil, NewXParserError(ErrMsgBadField, literal) 332 | } 333 | 334 | // Next we may find an alias name for the column. 335 | if tk, _ := p.scanIgnoreWhitespace(); tk == AS { 336 | // By using the "AS" keyword. 337 | tk, literal := p.scanIgnoreWhitespace() 338 | if tk != IDENTIFIER { 339 | return nil, NewXParserError(ErrMsgBadField, literal) 340 | } 341 | field.ColumnAlias = literal 342 | } else if tk == IDENTIFIER { 343 | // Or without keyword. 344 | field.ColumnAlias = literal 345 | } else { 346 | p.unscan() 347 | } 348 | // Finally, add this field with the others. 349 | stmt.Fields = append(stmt.Fields, field) 350 | 351 | // If the next token is not a comma then break the loop. 352 | if tk, _ := p.scanIgnoreWhitespace(); tk != COMMA { 353 | p.unscan() 354 | break 355 | } 356 | } 357 | 358 | // Next we should see the "FROM" keyword. 359 | if tk, _ := p.scanIgnoreWhitespace(); tk != FROM { 360 | return nil, NewParserError(ErrMsgMissingSrc) 361 | } 362 | 363 | // Next we should read the table name. 364 | tk, literal := p.scanIgnoreWhitespace() 365 | if tk != IDENTIFIER { 366 | return nil, NewXParserError(ErrMsgBadSrc, literal) 367 | } 368 | stmt.TableName = literal 369 | 370 | // Newt we may read a "WHERE" keyword. 371 | if tk, _ := p.scanIgnoreWhitespace(); tk == WHERE { 372 | for { 373 | // Parse each condition, begin by the column name. 374 | cond := &Where{Column: &Column{}} 375 | tk, literal := p.scanIgnoreWhitespace() 376 | if tk != IDENTIFIER { 377 | return nil, NewXParserError(ErrMsgBadField, literal) 378 | } 379 | cond.ColumnName = literal 380 | 381 | // Expects the operator. 382 | tk, literal = p.scanIgnoreWhitespace() 383 | if !isOperator(tk) { 384 | return nil, NewXParserError(ErrMsgSyntax, literal) 385 | } 386 | cond.Sign = literal 387 | 388 | // And the value of the condition.ValueLiteral | String | ValueLiteralList | StringList 389 | tk, literal = p.scanIgnoreWhitespace() 390 | switch tk { 391 | case DECIMAL, DIGIT, VALUE_LITERAL: 392 | cond.IsValueLiteral = true 393 | fallthrough 394 | case STRING: 395 | cond.ColumnValue = append(cond.ColumnValue, literal) 396 | case LEFT_SQUARE_BRACKETS: 397 | p.unscan() 398 | if tk, cond.ColumnValue = p.scanValueList(); tk != VALUE_LITERAL_LIST && tk != STRING_LIST { 399 | return nil, NewXParserError(ErrMsgSyntax, literal) 400 | } else if tk == VALUE_LITERAL_LIST { 401 | cond.IsValueLiteral = true 402 | } 403 | default: 404 | return nil, NewXParserError(ErrMsgSyntax, literal) 405 | } 406 | stmt.Where = append(stmt.Where, cond) 407 | 408 | // If the next token is not an "AND" keyword then break the loop. 409 | if tk, _ := p.scanIgnoreWhitespace(); tk != AND { 410 | p.unscan() 411 | break 412 | } 413 | } 414 | } else { 415 | // No where clause. 416 | p.unscan() 417 | } 418 | 419 | // Next we may read a "DURING" keyword. 420 | if tk, _ := p.scanIgnoreWhitespace(); tk == DURING { 421 | var dateLiteral bool 422 | for { 423 | // Read the field used to group. 424 | tk, literal := p.scanIgnoreWhitespace() 425 | if tk == DIGIT && isDate(literal) { 426 | stmt.During = append(stmt.During, literal) 427 | } else if tk == IDENTIFIER && isDateRangeLiteral(literal) { 428 | stmt.During = append(stmt.During, literal) 429 | dateLiteral = true 430 | } else { 431 | return nil, NewXParserError(ErrMsgBadDuring, literal) 432 | } 433 | // If the next token is not a comma then break the loop. 434 | if tk, _ := p.scanIgnoreWhitespace(); tk != COMMA { 435 | p.unscan() 436 | break 437 | } 438 | } 439 | // Checks expected bounds. 440 | if rangeSize := len(stmt.During); rangeSize > 2 { 441 | return nil, NewXParserError(ErrMsgBadDuring, ErrMsgDuringSize) 442 | } else if rangeSize == 1 && !dateLiteral { 443 | return nil, NewXParserError(ErrMsgBadDuring, ErrMsgDuringLitSize) 444 | } else if rangeSize == 2 && dateLiteral { 445 | return nil, NewXParserError(ErrMsgBadDuring, ErrMsgDuringDateSize) 446 | } 447 | } else { 448 | // No during clause. 449 | p.unscan() 450 | } 451 | 452 | // Next we may see a "GROUP" keyword. 453 | if tk, _ := p.scanIgnoreWhitespace(); tk == GROUP { 454 | if tk, literal := p.scanIgnoreWhitespace(); tk != BY { 455 | return nil, NewXParserError(ErrMsgBadGroup, literal) 456 | } 457 | for { 458 | // Read the field used to group. 459 | tk, literal := p.scanIgnoreWhitespace() 460 | if tk != IDENTIFIER && tk != DIGIT { 461 | return nil, NewXParserError(ErrMsgBadGroup, literal) 462 | } 463 | // Check if the column exists as field. 464 | groupBy, err := stmt.searchColumn(literal) 465 | if err != nil { 466 | return nil, NewXParserError(ErrMsgBadGroup, err.Error()) 467 | } 468 | stmt.GroupBy = append(stmt.GroupBy, groupBy) 469 | 470 | // If the next token is not a comma then break the loop. 471 | if tk, _ := p.scanIgnoreWhitespace(); tk != COMMA { 472 | p.unscan() 473 | break 474 | } 475 | } 476 | } else { 477 | // No grouping clause. 478 | p.unscan() 479 | } 480 | 481 | // Next we may see a "ORDER" keyword. 482 | if tk, _ := p.scanIgnoreWhitespace(); tk == ORDER { 483 | if tk, literal := p.scanIgnoreWhitespace(); tk != BY { 484 | return nil, NewXParserError(ErrMsgBadOrder, literal) 485 | } 486 | for { 487 | // Read the field used to order. 488 | tk, literal := p.scanIgnoreWhitespace() 489 | if tk != IDENTIFIER && tk != DIGIT { 490 | return nil, NewXParserError(ErrMsgBadOrder, literal) 491 | } 492 | 493 | // Check if the column exists as field. 494 | orderBy := &Order{} 495 | column, err := stmt.searchColumn(literal) 496 | if err != nil { 497 | return nil, err 498 | } 499 | orderBy.ColumnPosition = column 500 | 501 | // Then, we may find a DESC or ASC keywords. 502 | if tk, _ = p.scanIgnoreWhitespace(); tk == DESC { 503 | orderBy.SortDesc = true 504 | } else if tk != ASC { 505 | p.unscan() 506 | } 507 | stmt.OrderBy = append(stmt.OrderBy, orderBy) 508 | 509 | // If the next token is not a comma then break the loop. 510 | if tk, _ := p.scanIgnoreWhitespace(); tk != COMMA { 511 | p.unscan() 512 | break 513 | } 514 | } 515 | } else { 516 | // No ordering clause. 517 | p.unscan() 518 | } 519 | 520 | // Next we may see a "LIMIT" keyword. 521 | if tk, _ := p.scanIgnoreWhitespace(); tk == LIMIT { 522 | var literal string 523 | if tk, literal = p.scanIgnoreWhitespace(); tk != DIGIT { 524 | return nil, NewXParserError(ErrMsgBadLimit, literal) 525 | } 526 | offset, _ := strconv.Atoi(literal) 527 | stmt.WithRowCount = true 528 | 529 | // If the next token is a comma then we should get the row count. 530 | if tk, _ := p.scanIgnoreWhitespace(); tk == COMMA { 531 | tk, literal := p.scanIgnoreWhitespace() 532 | if tk != DIGIT { 533 | return nil, NewXParserError(ErrMsgBadLimit, stmt.RowCount) 534 | } 535 | stmt.Offset = offset 536 | stmt.RowCount, _ = strconv.Atoi(literal) 537 | } else { 538 | // No row count value, so the offset is finally the row count. 539 | stmt.RowCount = offset 540 | p.unscan() 541 | } 542 | } else { 543 | // No limit clause. 544 | p.unscan() 545 | } 546 | 547 | // Finally, we should find the end of the query. 548 | var err error 549 | if stmt.GModifier, err = p.scanQueryEnding(); err != nil { 550 | return nil, err 551 | } 552 | return stmt, nil 553 | } 554 | 555 | // searchColumn returns the column matching the search expression. 556 | func (s SelectStatement) searchColumn(expr string) (*ColumnPosition, error) { 557 | // If expr is a digit, search column by position. 558 | if pos, err := strconv.Atoi(expr); err == nil { 559 | if column, err := s.searchColumnByPosition(pos); err == nil { 560 | return column, nil 561 | } 562 | return nil, NewXParserError(ErrMsgBadColumn, expr) 563 | } 564 | // Otherwise fetch each column to find it by name or alias. 565 | for i, field := range s.Fields { 566 | field := field.(*DynamicColumn) 567 | if field.ColumnName == expr || field.ColumnAlias == expr { 568 | return NewColumnPosition(field.Column, (i + 1)), nil 569 | } 570 | } 571 | return nil, NewXParserError(ErrMsgBadColumn, expr) 572 | } 573 | 574 | // searchColumnByPosition returns the column matching the search position. 575 | func (s DataStatement) searchColumnByPosition(pos int) (*ColumnPosition, error) { 576 | if pos < 1 || pos > len(s.Fields) { 577 | return nil, NewXParserError(ErrMsgBadColumn, pos) 578 | } 579 | return NewColumnPosition(s.Fields[(pos-1)].(*DynamicColumn).Column, pos), nil 580 | } 581 | 582 | // scan returns the next token from the underlying scanner. 583 | // If a token has been unscanned then read that instead. 584 | func (p *Parser) scan() (Token, string) { 585 | if p.buf.n != 0 { 586 | p.buf.n = 0 587 | } else { 588 | // No token in the buffer so, read the next token from the scanner. 589 | p.buf.t, p.buf.l = p.s.Scan() 590 | } 591 | return p.buf.t, p.buf.l 592 | } 593 | 594 | // scanDistinct scans the next runes as column to use to group. 595 | func (p *Parser) scanDistinct(field *DynamicColumn) error { 596 | tk, literal := p.scanIgnoreWhitespace() 597 | if tk != IDENTIFIER { 598 | return NewXParserError(ErrMsgBadField, literal) 599 | } 600 | field.Unique = true 601 | field.ColumnName = literal 602 | 603 | return nil 604 | } 605 | 606 | // scanIgnoreWhitespace scans the next non-whitespace token. 607 | func (p *Parser) scanIgnoreWhitespace() (tk Token, literal string) { 608 | tk, literal = p.scan() 609 | if tk == WHITE_SPACE { 610 | return p.scan() 611 | } 612 | return 613 | } 614 | 615 | // scanList consumes all runes between left and right square brackets. 616 | // Use comma as separator to return a list of string or literal value. 617 | func (p *Parser) scanValueList() (tk Token, list []string) { 618 | // A list must begin with a left square brackets. 619 | if ctk, _ := p.scanIgnoreWhitespace(); ctk != LEFT_SQUARE_BRACKETS { 620 | return 621 | } 622 | // Get all values of the list and names the loop on it: L 623 | L: 624 | for { 625 | ctk, literal := p.scanIgnoreWhitespace() 626 | switch ctk { 627 | case EOF: 628 | tk = ILLEGAL 629 | break L 630 | case RIGHT_SQUARE_BRACKETS: 631 | // End of the list. 632 | break L 633 | case VALUE_LITERAL, IDENTIFIER, DECIMAL, DIGIT: 634 | // A list can only be string list or a value literal list but not the both. 635 | if tk == STRING_LIST { 636 | tk = ILLEGAL 637 | break L 638 | } 639 | // Consume as value literal. 640 | tk = VALUE_LITERAL_LIST 641 | case STRING: 642 | // A list can only be string list or a value literal list but not the both. 643 | if tk == VALUE_LITERAL_LIST { 644 | tk = ILLEGAL 645 | break L 646 | } 647 | tk = STRING_LIST 648 | case COMMA: 649 | continue L 650 | default: 651 | tk = ILLEGAL 652 | break L 653 | } 654 | list = append(list, literal) 655 | } 656 | return 657 | } 658 | 659 | // scanQueryEnding scans the next runes as query ending. 660 | // Return true if vertical output is required or error if it is not the end of the query. 661 | func (p *Parser) scanQueryEnding() (bool, error) { 662 | tk, literal := p.scanIgnoreWhitespace() 663 | switch tk { 664 | case G_MODIFIER: 665 | return true, nil 666 | case SEMICOLON, EOF: 667 | return false, nil 668 | default: 669 | p.unscan() 670 | } 671 | return false, NewXParserError(ErrMsgSyntax, literal) 672 | } 673 | 674 | // unscan pushes the previously read token back onto the buffer. 675 | func (p *Parser) unscan() { 676 | p.buf.n = 1 677 | } 678 | -------------------------------------------------------------------------------- /parser_internal_test.go: -------------------------------------------------------------------------------- 1 | package awqlparse 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // Ensure the parser can parse strings into CREATE VIEW Statement. 10 | func TestParser_ParseCreateView(t *testing.T) { 11 | var queryTests = []struct { 12 | q string 13 | stmt *CreateViewStatement 14 | err error 15 | }{ 16 | // Simple statement. 17 | { 18 | q: `CREATE VIEW CAMPAIGN_DAILY AS SELECT SUM(DISTINCT Cost) FROM CAMPAIGN_PERFORMANCE_REPORT`, 19 | stmt: &CreateViewStatement{ 20 | DataStatement: DataStatement{ 21 | TableName: "CAMPAIGN_DAILY", 22 | }, 23 | View: &SelectStatement{ 24 | DataStatement: DataStatement{ 25 | Fields: []DynamicField{ 26 | &DynamicColumn{Column: &Column{ColumnName: "Cost"}, Method: "SUM", Unique: true}, 27 | }, 28 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 29 | }, 30 | }, 31 | }, 32 | }, 33 | 34 | // Replace statement with explicit column names. 35 | { 36 | q: `CREATE OR REPLACE VIEW CAMPAIGN_DAILY (Date, Adspend) AS SELECT Date, SUM(DISTINCT Cost) FROM CAMPAIGN_PERFORMANCE_REPORT GROUP BY 1`, 37 | stmt: &CreateViewStatement{ 38 | DataStatement: DataStatement{ 39 | TableName: "CAMPAIGN_DAILY", 40 | Fields: []DynamicField{ 41 | &DynamicColumn{&Column{ColumnName: "Date"}, "", false}, 42 | &DynamicColumn{&Column{ColumnName: "Adspend"}, "", false}, 43 | }, 44 | }, 45 | View: &SelectStatement{ 46 | DataStatement: DataStatement{ 47 | Fields: []DynamicField{ 48 | &DynamicColumn{&Column{ColumnName: "Date"}, "", false}, 49 | &DynamicColumn{&Column{ColumnName: "Cost"}, "SUM", true}, 50 | }, 51 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 52 | }, 53 | GroupBy: []FieldPosition{ 54 | &ColumnPosition{&Column{ColumnName: "Date"}, 1}, 55 | }, 56 | }, 57 | Replace: true, 58 | }, 59 | }, 60 | 61 | // Errors 62 | {q: `SELECT`, err: NewXParserError(ErrMsgBadMethod, "SELECT")}, 63 | {q: `CREATE VIEW !`, err: NewXParserError(ErrMsgBadSrc, "!")}, 64 | {q: `CREATE VIEW CAMPAIGN_DAILY (Name, Cost) AS SELECT SUM(DISTINCT Cost) FROM CAMPAIGN_PERFORMANCE_REPORT`, err: NewParserError(ErrMsgColumnsNotMatch)}, 65 | } 66 | 67 | for i, qt := range queryTests { 68 | stmt, err := NewParser(strings.NewReader(qt.q)).ParseCreateView() 69 | if err != nil { 70 | if qt.err.Error() != err.Error() { 71 | t.Errorf("%d. Expected the error message %s with %s, received %s", i, qt.err, qt.q, err) 72 | } 73 | } else if qt.err != nil { 74 | t.Errorf("%d. Expected the error message %v with %s, received no error", i, qt.err, qt.q) 75 | } else if !reflect.DeepEqual(qt.stmt, stmt) { 76 | t.Errorf("%d. Expected %#v, received %#v", i, qt.stmt, stmt) 77 | } 78 | } 79 | } 80 | 81 | // Ensure the parser can parse strings into DESCRIBE Statement. 82 | func TestParser_ParseDescribe(t *testing.T) { 83 | var queryTests = []struct { 84 | q string 85 | stmt *DescribeStatement 86 | err error 87 | }{ 88 | // Simple statement. 89 | { 90 | q: `DESC CAMPAIGN_PERFORMANCE_REPORT`, 91 | stmt: &DescribeStatement{ 92 | DataStatement: DataStatement{ 93 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 94 | }, 95 | }, 96 | }, 97 | 98 | // Simple statement with alias of the method. 99 | { 100 | q: `DESCRIBE CAMPAIGN_PERFORMANCE_REPORT`, 101 | stmt: &DescribeStatement{ 102 | DataStatement: DataStatement{ 103 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 104 | }, 105 | }, 106 | }, 107 | 108 | // Full statement. 109 | { 110 | q: `DESC FULL CAMPAIGN_PERFORMANCE_REPORT CampaignName\G`, 111 | stmt: &DescribeStatement{ 112 | FullStatement: FullStatement{Full: true}, 113 | DataStatement: DataStatement{ 114 | Fields: []DynamicField{ 115 | &DynamicColumn{&Column{ColumnName: "CampaignName"}, "", false}, 116 | }, 117 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 118 | Statement: Statement{GModifier: true}, 119 | }, 120 | }, 121 | }, 122 | 123 | // Errors 124 | {q: `SELECT`, err: NewXParserError(ErrMsgBadMethod, "SELECT")}, 125 | {q: `DESC !`, err: NewXParserError(ErrMsgBadSrc, "!")}, 126 | } 127 | 128 | for i, qt := range queryTests { 129 | stmt, err := NewParser(strings.NewReader(qt.q)).ParseDescribe() 130 | if err != nil { 131 | if qt.err.Error() != err.Error() { 132 | t.Errorf("%d. Expected the error message %v with %s, received %v", i, qt.err, qt.q, err.Error()) 133 | } 134 | } else if qt.err != nil { 135 | t.Errorf("%d. Expected the error message %v with %s, received no error", i, qt.err, qt.q) 136 | } else if !reflect.DeepEqual(qt.stmt, stmt) { 137 | t.Errorf("%d. Expected %#v, received %#v", i, qt.stmt, stmt) 138 | } 139 | } 140 | } 141 | 142 | // Ensure the parser can parse strings into SHOW Statement. 143 | func TestParser_ParseShow(t *testing.T) { 144 | var queryTests = []struct { 145 | q string 146 | stmt *ShowStatement 147 | err error 148 | }{ 149 | // Simple statement. 150 | { 151 | q: `SHOW TABLES`, 152 | stmt: &ShowStatement{}, 153 | }, 154 | 155 | // Full statement. 156 | { 157 | q: `SHOW FULL TABLES\G`, 158 | stmt: &ShowStatement{ 159 | FullStatement: FullStatement{Full: true}, 160 | Statement: Statement{GModifier: true}, 161 | }, 162 | }, 163 | 164 | // Show statement like something as prefix. 165 | { 166 | q: `SHOW TABLES LIKE 'CAMPAIGN%'\G`, 167 | stmt: &ShowStatement{ 168 | Statement: Statement{GModifier: true}, 169 | Like: Pattern{Prefix: "CAMPAIGN"}, 170 | }, 171 | }, 172 | 173 | // Show statement like something as suffix. 174 | { 175 | q: `SHOW TABLES LIKE '%REPORT'\G`, 176 | stmt: &ShowStatement{ 177 | Statement: Statement{GModifier: true}, 178 | Like: Pattern{Suffix: "REPORT"}, 179 | }, 180 | }, 181 | 182 | // Show statement like something. 183 | { 184 | q: `SHOW TABLES LIKE '%NEGATIVE%'`, 185 | stmt: &ShowStatement{ 186 | Like: Pattern{Contains: "NEGATIVE"}, 187 | }, 188 | }, 189 | 190 | // Show statement named something. 191 | { 192 | q: `SHOW TABLES LIKE 'LABEL';`, 193 | stmt: &ShowStatement{ 194 | Like: Pattern{Equal: "LABEL"}, 195 | }, 196 | }, 197 | 198 | // Show statement with a specific column. 199 | { 200 | q: `SHOW TABLES WITH CampaignName;`, 201 | stmt: &ShowStatement{ 202 | With: "CampaignName", 203 | UseWith: true, 204 | }, 205 | }, 206 | 207 | // Show statement with a specific column. 208 | { 209 | q: `SHOW TABLES WITH "CampaignName";`, 210 | stmt: &ShowStatement{ 211 | With: "CampaignName", 212 | UseWith: true, 213 | }, 214 | }, 215 | 216 | // Show statement with no column. 217 | { 218 | q: `SHOW TABLES WITH "";`, 219 | stmt: &ShowStatement{ 220 | With: "", 221 | UseWith: true, 222 | }, 223 | }, 224 | 225 | // Errors 226 | {q: `SELECT`, err: NewXParserError(ErrMsgBadMethod, "SELECT")}, 227 | {q: `SHOW`, err: NewXParserError(ErrMsgSyntax, "")}, 228 | {q: `SHOW TABLES LIKE rv`, err: NewXParserError(ErrMsgSyntax, "rv")}, 229 | {q: `SHOW TABLES LABEL`, err: NewXParserError(ErrMsgSyntax, "LABEL")}, 230 | } 231 | 232 | for i, qt := range queryTests { 233 | stmt, err := NewParser(strings.NewReader(qt.q)).ParseShow() 234 | if err != nil { 235 | if qt.err.Error() != err.Error() { 236 | t.Errorf("%d. Expected the error message %v with %s, received %v", i, qt.err, qt.q, err.Error()) 237 | } 238 | } else if qt.err != nil { 239 | t.Errorf("%d. Expected the error message %v with %s, received no error", i, qt.err, qt.q) 240 | } else if !reflect.DeepEqual(qt.stmt, stmt) { 241 | t.Errorf("%d. Expected %#v, received %#v", i, qt.stmt, stmt) 242 | } 243 | } 244 | } 245 | 246 | // Ensure the parser can parse strings into SELECT Statement. 247 | func TestParser_ParseSelect(t *testing.T) { 248 | var queryTests = []struct { 249 | q string 250 | stmt *SelectStatement 251 | err error 252 | }{ 253 | // Single field statement. 254 | { 255 | q: `SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT`, 256 | stmt: &SelectStatement{ 257 | DataStatement: DataStatement{ 258 | Fields: []DynamicField{ 259 | &DynamicColumn{&Column{ColumnName: "CampaignName"}, "", false}, 260 | }, 261 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 262 | }, 263 | }, 264 | }, 265 | 266 | // Multi-fields statement with vertical display. 267 | { 268 | q: `SELECT CampaignId, CampaignName, Cost FROM CAMPAIGN_PERFORMANCE_REPORT\G`, 269 | stmt: &SelectStatement{ 270 | DataStatement: DataStatement{ 271 | Fields: []DynamicField{ 272 | &DynamicColumn{&Column{ColumnName: "CampaignId"}, "", false}, 273 | &DynamicColumn{&Column{ColumnName: "CampaignName"}, "", false}, 274 | &DynamicColumn{&Column{ColumnName: "Cost"}, "", false}, 275 | }, 276 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 277 | Statement: Statement{GModifier: true}, 278 | }, 279 | }, 280 | }, 281 | 282 | // Select all statement on view with condition and range date. 283 | { 284 | q: `SELECT * FROM CAMPAIGN_DAILY WHERE CampaignId = 12345678 DURING YESTERDAY;`, 285 | stmt: &SelectStatement{ 286 | DataStatement: DataStatement{ 287 | Fields: []DynamicField{ 288 | &DynamicColumn{&Column{ColumnName: "*"}, "", false}, 289 | }, 290 | TableName: "CAMPAIGN_DAILY", 291 | }, 292 | Where: []Condition{ 293 | &Where{&Column{ColumnName: "CampaignId"}, "=", []string{"12345678"}, true}, 294 | }, 295 | During: []string{"YESTERDAY"}, 296 | }, 297 | }, 298 | 299 | // Select statement with aggregate function and alias with row count limit. 300 | { 301 | q: `SELECT MAX(Cost) as max FROM CAMPAIGN_PERFORMANCE_REPORT LIMIT 5\G`, 302 | stmt: &SelectStatement{ 303 | DataStatement: DataStatement{ 304 | Fields: []DynamicField{ 305 | &DynamicColumn{&Column{ColumnName: "Cost", ColumnAlias: "max"}, "MAX", false}, 306 | }, 307 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 308 | Statement: Statement{GModifier: true}, 309 | }, 310 | Limit: Limit{0, 5, true}, 311 | }, 312 | }, 313 | 314 | // Select statement with aggregate function with distinct inside. 315 | { 316 | q: `SELECT SUM(distinct Cost) FROM CAMPAIGN_PERFORMANCE_REPORT`, 317 | stmt: &SelectStatement{ 318 | DataStatement: DataStatement{ 319 | Fields: []DynamicField{ 320 | &DynamicColumn{&Column{ColumnName: "Cost"}, "SUM", true}, 321 | }, 322 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 323 | }, 324 | }, 325 | }, 326 | 327 | // Select statement with distinct column with alias, ordering and limit with offset and row count. 328 | { 329 | q: `SELECT DISTINCT Cost as c FROM CAMPAIGN_PERFORMANCE_REPORT DURING 20161224,20161224 ORDER BY 1 DESC LIMIT 15, 5;`, 330 | stmt: &SelectStatement{ 331 | DataStatement: DataStatement{ 332 | Fields: []DynamicField{ 333 | &DynamicColumn{&Column{ColumnName: "Cost", ColumnAlias: "c"}, "", true}, 334 | }, 335 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 336 | }, 337 | During: []string{"20161224", "20161224"}, 338 | OrderBy: []Orderer{ 339 | &Order{&ColumnPosition{&Column{ColumnName: "Cost", ColumnAlias: "c"}, 1}, true}, 340 | }, 341 | Limit: Limit{15, 5, true}, 342 | }, 343 | }, 344 | 345 | // Select statement with group by and string value list. 346 | { 347 | q: `SELECT Date, Cost FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignStatus IN ["ENABLED","PAUSED"] DURING LAST_WEEK GROUP BY 1;`, 348 | stmt: &SelectStatement{ 349 | DataStatement: DataStatement{ 350 | Fields: []DynamicField{ 351 | &DynamicColumn{&Column{ColumnName: "Date"}, "", false}, 352 | &DynamicColumn{&Column{ColumnName: "Cost"}, "", false}, 353 | }, 354 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 355 | }, 356 | Where: []Condition{ 357 | &Where{&Column{ColumnName: "CampaignStatus"}, "IN", []string{"ENABLED", "PAUSED"}, false}, 358 | }, 359 | During: []string{"LAST_WEEK"}, 360 | GroupBy: []FieldPosition{ 361 | &ColumnPosition{&Column{ColumnName: "Date"}, 1}, 362 | }, 363 | }, 364 | }, 365 | 366 | // Select statement with value literal list and EOF as ending. 367 | { 368 | q: `SELECT Cost FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignId IN [123456789,987654321]`, 369 | stmt: &SelectStatement{ 370 | DataStatement: DataStatement{ 371 | Fields: []DynamicField{ 372 | &DynamicColumn{&Column{ColumnName: "Cost"}, "", false}, 373 | }, 374 | TableName: "CAMPAIGN_PERFORMANCE_REPORT", 375 | }, 376 | Where: []Condition{ 377 | &Where{&Column{ColumnName: "CampaignId"}, "IN", []string{"123456789", "987654321"}, true}, 378 | }, 379 | }, 380 | }, 381 | 382 | // Errors 383 | {q: `DELETE`, err: NewXParserError(ErrMsgBadMethod, "DELETE")}, 384 | {q: `SELECT !`, err: NewXParserError(ErrMsgBadField, "!")}, 385 | {q: `SELECT CampaignId Impressions`, err: NewParserError(ErrMsgMissingSrc)}, 386 | {q: `SELECT CampaignId FROM`, err: NewXParserError(ErrMsgBadSrc, "")}, 387 | {q: `SELECT CampaignId FROM REPORT WHERE`, err: NewXParserError(ErrMsgBadField, "")}, 388 | {q: `SELECT CampaignId FROM REPORT GROUP`, err: NewXParserError(ErrMsgBadGroup, "")}, 389 | {q: `SELECT CampaignId FROM REPORT GROUP BY ,`, err: NewXParserError(ErrMsgBadGroup, ",")}, 390 | {q: `SELECT CampaignId FROM REPORT GROUP BY 2`, err: NewXParserError(ErrMsgBadGroup, NewXParserError(ErrMsgBadColumn, "2"))}, 391 | {q: `SELECT CampaignId FROM REPORT ORDER 1`, err: NewXParserError(ErrMsgBadOrder, "1")}, 392 | {q: `SELECT CampaignId FROM REPORT LIMIT 1 SELECT`, err: NewXParserError(ErrMsgSyntax, "SELECT")}, 393 | {q: `SELECT CampaignId FROM REPORT LIMIT`, err: NewXParserError(ErrMsgBadLimit, "")}, 394 | {q: `SELECT DISTINCT 1 FROM CAMPAIGN_PERFORMANCE_REPORT`, err: NewXParserError(ErrMsgBadField, "1")}, 395 | {q: `SELECT rv(Cost) FROM CAMPAIGN_PERFORMANCE_REPORT`, err: NewXParserError(ErrMsgBadFunc, "rv")}, 396 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignName ! "rv"`, err: NewXParserError(ErrMsgSyntax, "!")}, 397 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignName = !`, err: NewXParserError(ErrMsgSyntax, "!")}, 398 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignName IN [ !`, err: NewXParserError(ErrMsgSyntax, "[")}, 399 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT DURING`, err: NewXParserError(ErrMsgBadDuring, "")}, 400 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT DURING RV`, err: NewXParserError(ErrMsgBadDuring, "RV")}, 401 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT DURING TODAY, YESTERDAY`, err: NewXParserError(ErrMsgBadDuring, ErrMsgDuringDateSize)}, 402 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT DURING 201612`, err: NewXParserError(ErrMsgBadDuring, "201612")}, 403 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT DURING 20161224`, err: NewXParserError(ErrMsgBadDuring, ErrMsgDuringLitSize)}, 404 | {q: `SELECT CampaignId FROM CAMPAIGN_PERFORMANCE_REPORT DURING 20161224,20161225,20161226`, err: NewXParserError(ErrMsgBadDuring, ErrMsgDuringSize)}, 405 | {q: `SELECT Cost FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignStatus IN ["ENABLED",PAUSED];`, err: NewXParserError(ErrMsgSyntax, "[")}, 406 | {q: `SELECT Cost FROM CAMPAIGN_PERFORMANCE_REPORT WHERE CampaignStatus IN [PAUSED,"ENABLED"];`, err: NewXParserError(ErrMsgSyntax, "[")}, 407 | } 408 | 409 | for i, qt := range queryTests { 410 | stmt, err := NewParser(strings.NewReader(qt.q)).ParseSelect() 411 | if err != nil { 412 | if qt.err.Error() != err.Error() { 413 | t.Errorf("%d. Expected the error message %v with %s, received %v", i, qt.err, qt.q, err.Error()) 414 | } 415 | } else if qt.err != nil { 416 | t.Errorf("%d. Expected the error message %v with %s, received no error", i, qt.err, qt.q) 417 | } else if !reflect.DeepEqual(qt.stmt, stmt) { 418 | t.Errorf("%d. Expected %#v, received %#v", i, qt.stmt, stmt) 419 | } 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package awqlparse_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | awql "github.com/rvflash/awql-parser" 8 | ) 9 | 10 | // Ensure the parser can parse statements correctly. 11 | func ExampleParser_Parse() { 12 | q := `SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT ORDER BY 1 LIMIT 5\GDESC ADGROUP_PERFORMANCE_REPORT AdGroupName;` 13 | stmts, _ := awql.NewParser(strings.NewReader(q)).Parse() 14 | for _, stmt := range stmts { 15 | switch stmt.(type) { 16 | case awql.SelectStmt: 17 | fmt.Println(stmt.(awql.SelectStmt).OrderList()[0].Name()) 18 | case awql.DescribeStmt: 19 | fmt.Println(stmt.(awql.DescribeStmt).SourceName()) 20 | fmt.Println(stmt.(awql.DescribeStmt).Columns()[0].Name()) 21 | } 22 | } 23 | // Output: 24 | // CampaignName 25 | // ADGROUP_PERFORMANCE_REPORT 26 | // AdGroupName 27 | } 28 | 29 | // Ensure the parser can parse statements correctly and return only the first. 30 | func ExampleParser_ParseRow() { 31 | q := `SELECT CampaignName FROM CAMPAIGN_PERFORMANCE_REPORT;` 32 | stmt, _ := awql.NewParser(strings.NewReader(q)).ParseRow() 33 | if stmt, ok := stmt.(awql.SelectStmt); ok { 34 | fmt.Println(stmt.SourceName()) 35 | // Output: CAMPAIGN_PERFORMANCE_REPORT 36 | } 37 | } 38 | 39 | // Ensure the parser can parse select statement. 40 | func ExampleParser_ParseSelect() { 41 | q := `SELECT AdGroupName FROM ADGROUP_PERFORMANCE_REPORT;` 42 | stmt, _ := awql.NewParser(strings.NewReader(q)).ParseSelect() 43 | fmt.Printf("Gets the column named %v from %v.\n", stmt.Columns()[0].Name(), stmt.SourceName()) 44 | // Output: Gets the column named AdGroupName from ADGROUP_PERFORMANCE_REPORT. 45 | } 46 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package awqlparse 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // eof represents a marker rune for the end of the reader. 14 | var eof = rune(0) 15 | 16 | // Scanner represents a lexical scanner. 17 | type Scanner struct { 18 | r *bufio.Reader 19 | } 20 | 21 | // NewScanner returns a new instance of Scanner. 22 | func NewScanner(r io.Reader) *Scanner { 23 | return &Scanner{r: bufio.NewReader(r)} 24 | } 25 | 26 | // Scan returns the next token and literal value. 27 | func (s *Scanner) Scan() (Token, string) { 28 | // Get the next rune. 29 | r := s.read() 30 | if isWhitespace(r) { 31 | // Consume all contiguous whitespace. 32 | s.unread() 33 | return s.scanWhitespace() 34 | } else if isQuote(r) { 35 | // Consume as string. 36 | s.unread() 37 | return s.scanQuotedString() 38 | } else if isLetter(r) { 39 | // A keyword begins by a letter. 40 | // Consume as an identifier or reserved word. 41 | s.unread() 42 | return s.scanIdentifier() 43 | } else if isDigit(r) { 44 | // Consume as a number. 45 | s.unread() 46 | return s.scanNumber() 47 | } 48 | 49 | // Otherwise read the individual character. 50 | switch r { 51 | case eof: 52 | return EOF, "" 53 | case '*': 54 | return ASTERISK, string(r) 55 | case ',': 56 | return COMMA, string(r) 57 | case '(': 58 | return LEFT_PARENTHESIS, string(r) 59 | case ')': 60 | return RIGHT_PARENTHESIS, string(r) 61 | case '[': 62 | return LEFT_SQUARE_BRACKETS, string(r) 63 | case ']': 64 | return RIGHT_SQUARE_BRACKETS, string(r) 65 | case '=': 66 | return EQUAL, string(r) 67 | case '!': 68 | // Deal with != 69 | if r := s.read(); r == '=' { 70 | return DIFFERENT, "!=" 71 | } 72 | s.unread() 73 | case '>': 74 | // Deal with >= 75 | if r := s.read(); r == '=' { 76 | return SUPERIOR_OR_EQUAL, ">=" 77 | } 78 | s.unread() 79 | return SUPERIOR, string(r) 80 | case '<': 81 | // Deal with <= 82 | if r := s.read(); r == '=' { 83 | return INFERIOR_OR_EQUAL, "<=" 84 | } 85 | s.unread() 86 | return INFERIOR, string(r) 87 | case '\\': 88 | // Deal with \G or lowercase version. 89 | if r := s.read(); r == 'G' || r == 'g' { 90 | return G_MODIFIER, fmt.Sprintf("\\%c", r) 91 | } 92 | s.unread() 93 | case ';': 94 | return SEMICOLON, string(r) 95 | } 96 | return ILLEGAL, string(r) 97 | } 98 | 99 | // scanIdentifier consumes the current rune and all contiguous literal runes. 100 | func (s *Scanner) scanIdentifier() (Token, string) { 101 | // Create a buffer and read the current character into it. 102 | var buf bytes.Buffer 103 | buf.WriteRune(s.read()) 104 | 105 | // Read every subsequent character of this token into the buffer. 106 | // Non-literal characters or EOF will cause the loop to exit. 107 | var valueLiteral bool 108 | for { 109 | if r := s.read(); r == eof { 110 | break 111 | } else if !isValueLiteral(r) { 112 | s.unread() 113 | break 114 | } else { 115 | if r == '.' { 116 | valueLiteral = true 117 | } 118 | buf.WriteRune(r) 119 | } 120 | } 121 | 122 | // If the string is a value literal then return it. 123 | if valueLiteral { 124 | return VALUE_LITERAL, buf.String() 125 | } 126 | 127 | // If the string matches a reserved keyword then return it. 128 | switch strings.ToUpper(buf.String()) { 129 | case "DESCRIBE": 130 | return DESCRIBE, buf.String() 131 | case "SELECT": 132 | return SELECT, buf.String() 133 | case "CREATE": 134 | return CREATE, buf.String() 135 | case "REPLACE": 136 | return REPLACE, buf.String() 137 | case "VIEW": 138 | return VIEW, buf.String() 139 | case "SHOW": 140 | return SHOW, buf.String() 141 | case "FULL": 142 | return FULL, buf.String() 143 | case "TABLES": 144 | return TABLES, buf.String() 145 | case "DISTINCT": 146 | return DISTINCT, buf.String() 147 | case "AS": 148 | return AS, buf.String() 149 | case "FROM": 150 | return FROM, buf.String() 151 | case "WHERE": 152 | return WHERE, buf.String() 153 | case "LIKE": 154 | return LIKE, buf.String() 155 | case "WITH": 156 | return WITH, buf.String() 157 | case "AND": 158 | return AND, buf.String() 159 | case "OR": 160 | return OR, buf.String() 161 | case "IN": 162 | return IN, buf.String() 163 | case "NOT_IN": 164 | return NOT_IN, buf.String() 165 | case "STARTS_WITH": 166 | return STARTS_WITH, buf.String() 167 | case "STARTS_WITH_IGNORE_CASE": 168 | return STARTS_WITH_IGNORE_CASE, buf.String() 169 | case "CONTAINS": 170 | return CONTAINS, buf.String() 171 | case "CONTAINS_IGNORE_CASE": 172 | return CONTAINS_IGNORE_CASE, buf.String() 173 | case "DOES_NOT_CONTAIN": 174 | return DOES_NOT_CONTAIN, buf.String() 175 | case "DOES_NOT_CONTAIN_IGNORE_CASE": 176 | return DOES_NOT_CONTAIN_IGNORE_CASE, buf.String() 177 | case "DURING": 178 | return DURING, buf.String() 179 | case "GROUP": 180 | return GROUP, buf.String() 181 | case "ORDER": 182 | return ORDER, buf.String() 183 | case "BY": 184 | return BY, buf.String() 185 | case "ASC": 186 | return ASC, buf.String() 187 | case "DESC": 188 | return DESC, buf.String() 189 | case "LIMIT": 190 | return LIMIT, buf.String() 191 | } 192 | return IDENTIFIER, buf.String() 193 | } 194 | 195 | // scanNumber consumes all digit or dot runes. 196 | func (s *Scanner) scanNumber() (tk Token, str string) { 197 | // Create a buffer and read the current character into it. 198 | var buf bytes.Buffer 199 | for { 200 | if r := s.read(); r == eof { 201 | break 202 | } else if !isDigit(r) && r != '.' { 203 | s.unread() 204 | break 205 | } else { 206 | buf.WriteRune(r) 207 | } 208 | } 209 | // Check if it is a valid number. 210 | str = buf.String() 211 | if _, err := strconv.Atoi(str); err == nil { 212 | tk = DIGIT 213 | } else if _, err := strconv.ParseFloat(str, 64); err == nil { 214 | tk = DECIMAL 215 | } 216 | return 217 | } 218 | 219 | // scanQuotedString consumes the current rune and all runes after it 220 | // until the next unprotected quote character. 221 | func (s *Scanner) scanQuotedString() (Token, string) { 222 | // Create a buffer and add the single or double quote into it. 223 | quote := s.read() 224 | if quote != '\'' && quote != '"' { 225 | return ILLEGAL, string(quote) 226 | } 227 | var buf bytes.Buffer 228 | for { 229 | r := s.read() 230 | if r == eof { 231 | return ILLEGAL, buf.String() 232 | } else if r == '\\' { 233 | buf.WriteRune(r) 234 | // Only the character immediately after the escape can itself be a backslash or quote. 235 | // Thus, we only need to protect the first character after the backslash. 236 | buf.WriteRune(s.read()) 237 | } else if r != quote { 238 | buf.WriteRune(r) 239 | } else { 240 | break 241 | } 242 | } 243 | return STRING, buf.String() 244 | } 245 | 246 | // scanWhitespace consumes the current rune and all contiguous whitespace. 247 | func (s *Scanner) scanWhitespace() (Token, string) { 248 | var buf bytes.Buffer 249 | for { 250 | if r := s.read(); r == eof { 251 | break 252 | } else if !isWhitespace(r) { 253 | s.unread() 254 | break 255 | } else { 256 | buf.WriteRune(r) 257 | } 258 | } 259 | return WHITE_SPACE, buf.String() 260 | } 261 | 262 | // read reads the next rune from the bufferred reader. 263 | // Returns the rune(0) if an error occurs (or io.EOF is returned). 264 | func (s *Scanner) read() rune { 265 | ch, _, err := s.r.ReadRune() 266 | if err != nil { 267 | return eof 268 | } 269 | return ch 270 | } 271 | 272 | // unread places the previously read rune back on the reader. 273 | func (s *Scanner) unread() { 274 | _ = s.r.UnreadRune() 275 | } 276 | 277 | // isDate return true if the string is a date as expected by Adwords. 278 | func isDate(s string) bool { 279 | if _, err := time.Parse("20060102", s); err == nil { 280 | return true 281 | } 282 | return false 283 | } 284 | 285 | // isDateRange return true if the string is a date range literal. 286 | func isDateRangeLiteral(s string) bool { 287 | switch s { 288 | case "TODAY", "YESTERDAY", 289 | "THIS_WEEK_SUN_TODAY", "THIS_WEEK_MON_TODAY", 290 | "LAST_WEEK", "LAST_7_DAYS", "LAST_14_DAYS", 291 | "LAST_30_DAYS", "LAST_BUSINESS_WEEK", 292 | "LAST_WEEK_SUN_SAT", "THIS_MONTH": 293 | return true 294 | } 295 | return false 296 | } 297 | 298 | // isDigit returns true if the rune is a digit. 299 | func isDigit(r rune) bool { 300 | return (r >= '0' && r <= '9') 301 | } 302 | 303 | // isFunction returns true if it is an aggregate function. 304 | func isFunction(s string) bool { 305 | switch strings.ToUpper(s) { 306 | case "AVG", "COUNT", "MAX", "MIN", "SUM": 307 | return true 308 | } 309 | return false 310 | } 311 | 312 | // isLetter returns true if the rune is a letter. 313 | func isLetter(r rune) bool { 314 | return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') 315 | } 316 | 317 | // isLiteral returns true if the rune is a literal [a-zA-Z0-9_] 318 | func isLiteral(r rune) bool { 319 | return r == '_' || isDigit(r) || isLetter(r) 320 | } 321 | 322 | // isOperator returns true if the token is an operator 323 | func isOperator(tk Token) bool { 324 | switch tk { 325 | case EQUAL, DIFFERENT, SUPERIOR, SUPERIOR_OR_EQUAL, 326 | INFERIOR, INFERIOR_OR_EQUAL, IN, NOT_IN, 327 | STARTS_WITH, STARTS_WITH_IGNORE_CASE, 328 | CONTAINS, CONTAINS_IGNORE_CASE, 329 | DOES_NOT_CONTAIN, DOES_NOT_CONTAIN_IGNORE_CASE: 330 | return true 331 | } 332 | return false 333 | } 334 | 335 | // isQuote returns if the rune is a single quote or double quote. 336 | func isQuote(r rune) bool { 337 | return r == '"' || r == '\'' 338 | } 339 | 340 | // isValueLiteral returns true if the rune is a value literal [a-zA-Z0-9_.] 341 | func isValueLiteral(r rune) bool { 342 | return r == '.' || isLiteral(r) 343 | } 344 | 345 | // isWhitespace returns true if the rune is a space, tab, or newline. 346 | func isWhitespace(r rune) bool { 347 | return r == ' ' || r == '\t' || r == '\n' 348 | } 349 | -------------------------------------------------------------------------------- /scanner_test.go: -------------------------------------------------------------------------------- 1 | package awqlparse_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | awql "github.com/rvflash/awql-parser" 8 | ) 9 | 10 | // Ensure the scanner can scan tokens correctly. 11 | func TestScanner_Scan(t *testing.T) { 12 | var tests = []struct { 13 | s string 14 | t awql.Token 15 | l string 16 | }{ 17 | // Special tokens (EOF, ILLEGAL, etc.) 18 | {s: ``, t: awql.EOF}, 19 | {s: `#`, t: awql.ILLEGAL, l: `#`}, 20 | {s: `8`, t: awql.DIGIT, l: `8`}, 21 | {s: `1.0`, t: awql.DECIMAL, l: `1.0`}, 22 | {s: `2.0b`, t: awql.DECIMAL, l: `2.0`}, 23 | {s: `\G`, t: awql.G_MODIFIER, l: `\G`}, 24 | {s: `\g`, t: awql.G_MODIFIER, l: `\g`}, 25 | {s: `\p`, t: awql.ILLEGAL, l: `\`}, 26 | 27 | // Misc characters 28 | {s: `*`, t: awql.ASTERISK, l: `*`}, 29 | {s: `,`, t: awql.COMMA, l: `,`}, 30 | {s: `(`, t: awql.LEFT_PARENTHESIS, l: `(`}, 31 | {s: `)`, t: awql.RIGHT_PARENTHESIS, l: `)`}, 32 | {s: `[`, t: awql.LEFT_SQUARE_BRACKETS, l: `[`}, 33 | {s: `]`, t: awql.RIGHT_SQUARE_BRACKETS, l: `]`}, 34 | {s: `;`, t: awql.SEMICOLON, l: `;`}, 35 | 36 | // Literal 37 | {s: ` `, t: awql.WHITE_SPACE, l: ` `}, 38 | {s: ` a`, t: awql.WHITE_SPACE, l: ` `}, 39 | {s: "\t", t: awql.WHITE_SPACE, l: "\t"}, 40 | {s: "\n", t: awql.WHITE_SPACE, l: "\n"}, 41 | {s: `'string'`, t: awql.STRING, l: `string`}, 42 | {s: `"string"`, t: awql.STRING, l: `string`}, 43 | {s: `"stri`, t: awql.ILLEGAL, l: `stri`}, 44 | {s: `"my \"tiny\" string"`, t: awql.STRING, l: `my \"tiny\" string`}, 45 | {s: `a.b`, t: awql.VALUE_LITERAL, l: `a.b`}, 46 | 47 | // Operator 48 | {s: `=`, t: awql.EQUAL, l: `=`}, 49 | {s: `!=`, t: awql.DIFFERENT, l: `!=`}, 50 | {s: `!-`, t: awql.ILLEGAL, l: `!`}, 51 | {s: `>`, t: awql.SUPERIOR, l: `>`}, 52 | {s: `>=`, t: awql.SUPERIOR_OR_EQUAL, l: `>=`}, 53 | {s: `<`, t: awql.INFERIOR, l: `<`}, 54 | {s: `<=`, t: awql.INFERIOR_OR_EQUAL, l: `<=`}, 55 | {s: `IN`, t: awql.IN, l: `IN`}, 56 | {s: `NOT_IN`, t: awql.NOT_IN, l: `NOT_IN`}, 57 | {s: `STARTS_WITH`, t: awql.STARTS_WITH, l: `STARTS_WITH`}, 58 | {s: `STARTS_WITH_IGNORE_CASE`, t: awql.STARTS_WITH_IGNORE_CASE, l: `STARTS_WITH_IGNORE_CASE`}, 59 | {s: `CONTAINS`, t: awql.CONTAINS, l: `CONTAINS`}, 60 | {s: `CONTAINS_IGNORE_CASE`, t: awql.CONTAINS_IGNORE_CASE, l: `CONTAINS_IGNORE_CASE`}, 61 | {s: `DOES_NOT_CONTAIN`, t: awql.DOES_NOT_CONTAIN, l: `DOES_NOT_CONTAIN`}, 62 | {s: `DOES_NOT_CONTAIN_IGNORE_CASE`, t: awql.DOES_NOT_CONTAIN_IGNORE_CASE, l: `DOES_NOT_CONTAIN_IGNORE_CASE`}, 63 | 64 | // Identifiers 65 | {s: `Criteria`, t: awql.IDENTIFIER, l: `Criteria`}, 66 | {s: `CRITERIA_PERFORMANCE_REPORT`, t: awql.IDENTIFIER, l: `CRITERIA_PERFORMANCE_REPORT`}, 67 | {s: `Z6P0_C3P0_-`, t: awql.IDENTIFIER, l: `Z6P0_C3P0_`}, 68 | 69 | // Keywords 70 | {s: `DESCRIBE`, t: awql.DESCRIBE, l: `DESCRIBE`}, 71 | {s: `select`, t: awql.SELECT, l: `select`}, 72 | {s: `CREATE`, t: awql.CREATE, l: `CREATE`}, 73 | {s: `replace`, t: awql.REPLACE, l: `replace`}, 74 | {s: `VIEW`, t: awql.VIEW, l: `VIEW`}, 75 | {s: `SHOW`, t: awql.SHOW, l: `SHOW`}, 76 | {s: `FULL`, t: awql.FULL, l: `FULL`}, 77 | {s: `TABLES`, t: awql.TABLES, l: `TABLES`}, 78 | {s: `DISTINCT`, t: awql.DISTINCT, l: `DISTINCT`}, 79 | {s: `AS`, t: awql.AS, l: `AS`}, 80 | {s: `FROM`, t: awql.FROM, l: `FROM`}, 81 | {s: `WHERE`, t: awql.WHERE, l: `WHERE`}, 82 | {s: `LIKE`, t: awql.LIKE, l: `LIKE`}, 83 | {s: `WITH`, t: awql.WITH, l: `WITH`}, 84 | {s: `AND`, t: awql.AND, l: `AND`}, 85 | {s: `OR`, t: awql.OR, l: `OR`}, 86 | {s: `DURING`, t: awql.DURING, l: `DURING`}, 87 | {s: `ORDER`, t: awql.ORDER, l: `ORDER`}, 88 | {s: `GROUP`, t: awql.GROUP, l: `GROUP`}, 89 | {s: `BY`, t: awql.BY, l: `BY`}, 90 | {s: `ASC`, t: awql.ASC, l: `ASC`}, 91 | {s: `DESC`, t: awql.DESC, l: `DESC`}, 92 | {s: `LIMIT`, t: awql.LIMIT, l: `LIMIT`}, 93 | } 94 | 95 | for i, tt := range tests { 96 | s := awql.NewScanner(strings.NewReader(tt.s)) 97 | tk, l := s.Scan() 98 | if tt.t != tk { 99 | t.Errorf("%d. %q token mismatch: exp=%q got=%q <%q>", i, tt.s, tt.t, tk, l) 100 | } else if tt.l != l { 101 | t.Errorf("%d. %q literal mismatch: exp=%q got=%q", i, tt.s, tt.l, l) 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /statement.go: -------------------------------------------------------------------------------- 1 | package awqlparse 2 | 3 | import "fmt" 4 | 5 | // Field is the interface that must be implemented by a column. 6 | type Field interface { 7 | Name() string 8 | Alias() string 9 | } 10 | 11 | // Column represents a column. 12 | // It implements the DynamicColumn interface. 13 | type Column struct { 14 | ColumnName, 15 | ColumnAlias string 16 | } 17 | 18 | // NewColumn returns a pointer to a new Column. 19 | func NewColumn(name, alias string) *Column { 20 | return &Column{ColumnName: name, ColumnAlias: alias} 21 | } 22 | 23 | // Name returns the column name. 24 | func (c *Column) Name() string { 25 | return c.ColumnName 26 | } 27 | 28 | // Alias returns the column alias. 29 | func (c *Column) Alias() string { 30 | return c.ColumnAlias 31 | } 32 | 33 | // FieldPosition is the interface that must be implemented by a query's column. 34 | type FieldPosition interface { 35 | Field 36 | Position() int 37 | } 38 | 39 | // ColumnPosition represents a column with its position in the query. 40 | // It implements the FieldPosition interface. 41 | type ColumnPosition struct { 42 | *Column 43 | ColumnPos int 44 | } 45 | 46 | // NewColumnPosition returns a pointer to a new ColumnPosition. 47 | func NewColumnPosition(col *Column, pos int) *ColumnPosition { 48 | return &ColumnPosition{Column: col, ColumnPos: pos} 49 | } 50 | 51 | // Position returns the position of the field in the query. 52 | func (c *ColumnPosition) Position() int { 53 | return c.ColumnPos 54 | } 55 | 56 | // DynamicField is the interface that must be implemented by a query's field. 57 | type DynamicField interface { 58 | Field 59 | UseFunction() (string, bool) 60 | Distinct() bool 61 | } 62 | 63 | // DynamicColumn represents a field. 64 | // It implements the DynamicField interface. 65 | type DynamicColumn struct { 66 | *Column 67 | Method string 68 | Unique bool 69 | } 70 | 71 | // NewDynamicColumn returns a pointer to a new DynamicColumn. 72 | func NewDynamicColumn(col *Column, method string, unique bool) *DynamicColumn { 73 | return &DynamicColumn{Column: col, Method: method, Unique: unique} 74 | } 75 | 76 | // UseFunction returns the name of the method to apply of the column. 77 | // The second parameter indicates if a method is used. 78 | func (c *DynamicColumn) UseFunction() (string, bool) { 79 | return c.Method, c.Method != "" 80 | } 81 | 82 | // Distinct returns true if the column value needs to be unique. 83 | func (c *DynamicColumn) Distinct() bool { 84 | return c.Unique 85 | } 86 | 87 | // Condition is the interface that must be implemented by a condition. 88 | type Condition interface { 89 | Field 90 | Operator() string 91 | Value() (value []string, literal bool) 92 | } 93 | 94 | // Where represents a condition in where clause. 95 | // It implements the Condition interface. 96 | type Where struct { 97 | *Column 98 | Sign string 99 | ColumnValue []string 100 | IsValueLiteral bool 101 | } 102 | 103 | // Operator returns the condition's operator 104 | func (c *Where) Operator() string { 105 | return c.Sign 106 | } 107 | 108 | // Value returns the column's value of the condition. 109 | func (c *Where) Value() ([]string, bool) { 110 | return c.ColumnValue, c.IsValueLiteral 111 | } 112 | 113 | // Pattern represents a LIKE clause. 114 | type Pattern struct { 115 | Equal, Prefix, Contains, Suffix string 116 | } 117 | 118 | // Orderer is the interface that must be implemented by an ordering. 119 | type Orderer interface { 120 | FieldPosition 121 | SortDescending() bool 122 | } 123 | 124 | // Order represents an order by clause. 125 | // It implements the Orderer interface. 126 | type Order struct { 127 | *ColumnPosition 128 | SortDesc bool 129 | } 130 | 131 | // SortDescending returns true if the column needs to be sort by desc. 132 | func (o *Order) SortDescending() bool { 133 | return o.SortDesc 134 | } 135 | 136 | // Limit represents a limit clause. 137 | type Limit struct { 138 | Offset, RowCount int 139 | WithRowCount bool 140 | } 141 | 142 | // Stmt formats the query output. 143 | type Stmt interface { 144 | VerticalOutput() bool 145 | fmt.Stringer 146 | } 147 | 148 | // Statement enables to format the query output. 149 | type Statement struct { 150 | GModifier bool 151 | } 152 | 153 | // VerticalOutput returns true if the G modifier is required. 154 | // It implements the Stmt interface. 155 | func (s Statement) VerticalOutput() bool { 156 | return s.GModifier 157 | } 158 | 159 | // DataStmt represents a AWQL base statement. 160 | // By design, only the SELECT statement is supported by Adwords. 161 | // The AWQL command line tool extends it with others SQL grammar. 162 | type DataStmt interface { 163 | Columns() []DynamicField 164 | SourceName() string 165 | Stmt 166 | } 167 | 168 | // DataStatement represents a AWQL base statement. 169 | // It implements the DataStmt interface. 170 | type DataStatement struct { 171 | Fields []DynamicField 172 | TableName string 173 | Statement 174 | } 175 | 176 | // Columns returns the list of table fields. 177 | func (s DataStatement) Columns() []DynamicField { 178 | return s.Fields 179 | } 180 | 181 | // SourceName returns the table's name. 182 | func (s DataStatement) SourceName() string { 183 | return s.TableName 184 | } 185 | 186 | /* 187 | SelectStmt exposes the interface of AWQL Select Statement 188 | 189 | This is a extended version of the original grammar in order to manage all 190 | the possibilities of the AWQL command line tool. 191 | 192 | SelectClause : SELECT ColumnList 193 | FromClause : FROM SourceName 194 | WhereClause : WHERE ConditionList 195 | DuringClause : DURING DateRange 196 | GroupByClause : GROUP BY Grouping (, Grouping)* 197 | OrderByClause : ORDER BY Order (, Order)* 198 | LimitClause : LIMIT StartIndex , PageSize 199 | 200 | ConditionList : Condition (AND Condition)* 201 | Condition : ColumnName Operator Value 202 | Value : ValueLiteral | String | ValueLiteralList | StringList 203 | Order : ColumnName (DESC | ASC)? 204 | DateRange : DateRangeLiteral | Date,Date 205 | ColumnList : ColumnName (, ColumnName)* 206 | ColumnName : Literal 207 | TableName : Literal 208 | StartIndex : Non-negative integer 209 | PageSize : Non-negative integer 210 | 211 | Operator : = | != | > | >= | < | <= | IN | NOT_IN | STARTS_WITH | STARTS_WITH_IGNORE_CASE | 212 | CONTAINS | CONTAINS_IGNORE_CASE | DOES_NOT_CONTAIN | DOES_NOT_CONTAIN_IGNORE_CASE 213 | String : StringSingleQ | StringDoubleQ 214 | StringSingleQ : '(char)' 215 | StringDoubleQ : "(char)" 216 | StringList : [ String (, String)* ] 217 | ValueLiteral : [a-zA-Z0-9_.]* 218 | ValueLiteralList : [ ValueLiteral (, ValueLiteral)* ] 219 | Literal : [a-zA-Z0-9_]* 220 | DateRangeLiteral : TODAY | YESTERDAY | LAST_7_DAYS | THIS_WEEK_SUN_TODAY | THIS_WEEK_MON_TODAY | LAST_WEEK | 221 | LAST_14_DAYS | LAST_30_DAYS | LAST_BUSINESS_WEEK | LAST_WEEK_SUN_SAT | THIS_MONTH 222 | Date : 8-digit integer: YYYYMMDD 223 | */ 224 | type SelectStmt interface { 225 | DataStmt 226 | ConditionList() []Condition 227 | DuringList() []string 228 | GroupList() []FieldPosition 229 | OrderList() []Orderer 230 | StartIndex() int 231 | PageSize() (int, bool) 232 | LegacyString() string 233 | } 234 | 235 | // SelectStatement represents a AWQL SELECT statement. 236 | // SELECT...FROM...WHERE...DURING...GROUP BY...ORDER BY...LIMIT... 237 | // It implements the SelectStmt interface. 238 | type SelectStatement struct { 239 | DataStatement 240 | Where []Condition 241 | During []string 242 | GroupBy []FieldPosition 243 | OrderBy []Orderer 244 | Limit 245 | } 246 | 247 | // ConditionList returns the condition list. 248 | func (s SelectStatement) ConditionList() []Condition { 249 | return s.Where 250 | } 251 | 252 | // DuringList returns the during (date range). 253 | func (s SelectStatement) DuringList() []string { 254 | return s.During 255 | } 256 | 257 | // GroupList returns the group by columns. 258 | func (s SelectStatement) GroupList() []FieldPosition { 259 | return s.GroupBy 260 | } 261 | 262 | // OrderList returns the order by columns. 263 | func (s SelectStatement) OrderList() []Orderer { 264 | return s.OrderBy 265 | } 266 | 267 | // StartIndex returns the start index. 268 | func (s SelectStatement) StartIndex() int { 269 | return s.Offset 270 | } 271 | 272 | // PageSize returns the row count. 273 | func (s SelectStatement) PageSize() (int, bool) { 274 | return s.RowCount, s.WithRowCount 275 | } 276 | 277 | /* 278 | CreateViewStmt exposes the interface of AWQL Create View Statement 279 | 280 | Not supported natively by Adwords API. Used by the following AWQL command line tool: 281 | https://github.com/rvflash/awql/ 282 | 283 | CreateClause : CREATE (OR REPLACE)* VIEW DestinationName (**(**ColumnList**)**)* 284 | FromClause : AS SelectClause 285 | */ 286 | type CreateViewStmt interface { 287 | DataStmt 288 | ReplaceMode() bool 289 | SourceQuery() SelectStmt 290 | } 291 | 292 | // CreateViewStatement represents a AWQL CREATE VIEW statement. 293 | // CREATE...OR REPLACE...VIEW...AS 294 | // It implements the CreateViewStmt interface. 295 | type CreateViewStatement struct { 296 | DataStatement 297 | Replace bool 298 | View *SelectStatement 299 | } 300 | 301 | // ReplaceMode returns true if it is required to replace the existing view. 302 | func (s CreateViewStatement) ReplaceMode() bool { 303 | return s.Replace 304 | } 305 | 306 | // SourceQuery returns the source query, base of the view to create. 307 | func (s CreateViewStatement) SourceQuery() SelectStmt { 308 | return s.View 309 | } 310 | 311 | // FullStmt proposes the full statement mode. 312 | type FullStmt interface { 313 | FullMode() bool 314 | } 315 | 316 | // FullStatement enables a AWQL FULL mode. 317 | // It implements the FullStmt interface. 318 | type FullStatement struct { 319 | Full bool 320 | } 321 | 322 | // FullMode returns true if the full display is required. 323 | func (s FullStatement) FullMode() bool { 324 | return s.Full 325 | } 326 | 327 | /* 328 | DescribeStmt exposes the interface of AWQL Describe Statement 329 | 330 | Not supported natively by Adwords API. Used by the following AWQL command line tool: 331 | https://github.com/rvflash/awql/ 332 | 333 | DescribeClause : (DESCRIBE | DESC) (FULL)* SourceName (ColumnName)* 334 | */ 335 | type DescribeStmt interface { 336 | DataStmt 337 | FullStmt 338 | } 339 | 340 | // DescribeStatement represents a AWQL DESC statement. 341 | // DESC...FULL 342 | // It implements the DescribeStmt interface. 343 | type DescribeStatement struct { 344 | FullStatement 345 | DataStatement 346 | } 347 | 348 | /* 349 | ShowStmt exposes the interface of AWQL Show Statement 350 | 351 | Not supported natively by Adwords API. Used by the following AWQL command line tool: 352 | https://github.com/rvflash/awql/ 353 | 354 | ShowClause : SHOW (FULL)* TABLES 355 | WithClause : WITH ColumnName 356 | LikeClause : LIKE String 357 | */ 358 | type ShowStmt interface { 359 | FullStmt 360 | LikePattern() (p Pattern, used bool) 361 | WithFieldName() (name string, used bool) 362 | Stmt 363 | } 364 | 365 | // ShowStatement represents a AWQL SHOW statement. 366 | // SHOW...FULL...TABLES...LIKE...WITH 367 | // It implements the ShowStmt interface. 368 | type ShowStatement struct { 369 | FullStatement 370 | Like Pattern 371 | With string 372 | UseWith bool 373 | Statement 374 | } 375 | 376 | // LikePattern returns the pattern used for a like query on the table list. 377 | // If the second parameter is on, the like clause has been used. 378 | func (s ShowStatement) LikePattern() (Pattern, bool) { 379 | var used bool 380 | switch { 381 | case s.Like.Equal != "": 382 | used = true 383 | case s.Like.Contains != "": 384 | used = true 385 | case s.Like.Prefix != "": 386 | used = true 387 | case s.Like.Suffix != "": 388 | used = true 389 | } 390 | return s.Like, used 391 | } 392 | 393 | // WithFieldName returns the column name used to search table with this column. 394 | func (s ShowStatement) WithFieldName() (string, bool) { 395 | return s.With, s.UseWith 396 | } 397 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | package awqlparse 2 | 3 | /* 4 | Base AWQL grammar 5 | https://developers.google.com/adwords/api/docs/guides/awql#grammar 6 | 7 | Extended AWQL grammar 8 | https://github.com/rvflash/awql/ 9 | */ 10 | 11 | // Token represents a lexical token. 12 | type Token int 13 | 14 | // List of special runes or reserved keywords. 15 | const ( 16 | // Special tokens 17 | ILLEGAL Token = iota 18 | EOF 19 | DIGIT // [0-9] 20 | DECIMAL // [0-9.] 21 | G_MODIFIER // \G ou \g 22 | 23 | // Literals 24 | IDENTIFIER // base element 25 | WHITE_SPACE // white space 26 | STRING // char between single or double quotes 27 | STRING_LIST 28 | VALUE_LITERAL // [a-zA-Z0-9_.] 29 | VALUE_LITERAL_LIST 30 | 31 | // Misc characters 32 | ASTERISK // * 33 | COMMA // , 34 | LEFT_PARENTHESIS // ( 35 | RIGHT_PARENTHESIS // ) 36 | LEFT_SQUARE_BRACKETS // [ 37 | RIGHT_SQUARE_BRACKETS // ] 38 | SEMICOLON // ; 39 | 40 | // Operator 41 | EQUAL // = 42 | DIFFERENT // != 43 | SUPERIOR // > 44 | SUPERIOR_OR_EQUAL // >= 45 | INFERIOR // < 46 | INFERIOR_OR_EQUAL // <= 47 | IN 48 | NOT_IN 49 | STARTS_WITH 50 | STARTS_WITH_IGNORE_CASE 51 | CONTAINS 52 | CONTAINS_IGNORE_CASE 53 | DOES_NOT_CONTAIN 54 | DOES_NOT_CONTAIN_IGNORE_CASE 55 | 56 | // Base keywords 57 | DESCRIBE 58 | SELECT 59 | CREATE 60 | REPLACE 61 | VIEW 62 | SHOW 63 | FULL 64 | TABLES 65 | DISTINCT 66 | AS 67 | FROM 68 | WHERE 69 | LIKE 70 | WITH 71 | AND 72 | OR 73 | DURING 74 | ORDER 75 | GROUP 76 | BY 77 | ASC 78 | DESC 79 | LIMIT 80 | ) 81 | --------------------------------------------------------------------------------