├── .github └── workflows │ ├── docker-publish.yml │ ├── end2end-tests.yml │ └── unit-tests.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── ast ├── ast.go └── doc.go ├── doc.go ├── e2e ├── e2e_test.sh ├── expected_outputs │ ├── 1_select_with_where_expected_output │ ├── 2_select_with_limit_and_offset_expected_output │ ├── 3_delete_expected_output │ ├── 4_orderby_expected_output │ ├── 5_update_expected_output │ ├── 6_select_distinct_expected_output │ ├── 7_drop_table_expected_output │ ├── 8_select_with_join_expected_output │ └── 9_aggregate_functions_expected_output └── test_files │ ├── 1_select_with_where_test │ ├── 2_select_with_limit_and_offset_test │ ├── 3_delete_test │ ├── 4_orderby_test │ ├── 5_update_test │ ├── 6_select_distinct_test │ ├── 7_drop_table_test │ ├── 8_select_with_join_test │ └── 9_aggregate_functions_test ├── engine ├── column.go ├── doc.go ├── engine.go ├── engine_error_handling_test.go ├── engine_test.go ├── engine_utils.go ├── errors.go ├── generic_value.go ├── generic_value_test.go ├── query_processor.go ├── row.go └── table.go ├── go.mod ├── helm └── GO4SQL │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ └── service.yaml │ └── values.yaml ├── lexer ├── doc.go ├── lexer.go └── lexer_test.go ├── main.go ├── modes ├── doc.go └── handler.go ├── parser ├── doc.go ├── errors.go ├── parser.go ├── parser_error_handling_test.go └── parser_test.go └── token ├── doc.go └── token.go /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: [ "main", "cicd/docker-image" ] 6 | tags: [ 'v*.*.*' ] 7 | 8 | env: 9 | IMAGE_NAME: go4sql 10 | IMAGE_TAG: latest 11 | REGISTRY: docker.io 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | id-token: write 19 | steps: 20 | 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Docker build 25 | run: docker build . --tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} 26 | 27 | - name: Docker login 28 | run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} 29 | 30 | - name: Docker tag 31 | run: docker image tag ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} 32 | 33 | - name: Docker push 34 | run: docker push ${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }} 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/end2end-tests.yml: -------------------------------------------------------------------------------- 1 | name: end2end-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | - '!master' 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go: [ '1.23.6', '1.24.0' ] 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - name: Build 26 | run: go build -v 27 | 28 | - name: Make Test Script Executable 29 | run: chmod +x e2e/e2e_test.sh 30 | 31 | - name: Run Tests 32 | run: e2e/e2e_test.sh 33 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | - '!master' 8 | pull_request: 9 | branches: [ main ] 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | go: [ '1.23.6', '1.24.0' ] 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go }} 24 | 25 | - name: Build 26 | run: go build -v ./... 27 | 28 | - name: Test 29 | run: go test -v ./... 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | GO4SQL 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY ./ast /app/ast 6 | COPY ./engine /app/engine 7 | COPY ./lexer /app/lexer 8 | COPY ./parser /app/parser 9 | COPY ./token /app/token 10 | COPY ./modes /app/modes 11 | 12 | COPY go.mod /app 13 | COPY main.go /app 14 | 15 | RUN go build -o go4sql-docker 16 | 17 | ENTRYPOINT ["./go4sql-docker"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2024 Sara Ryfczyńska, Paweł Krupski 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | gopher_GO4SQL 3 |

4 | 5 | # GO4SQL 6 | 7 |

8 | 9 | Unit-Tests Status 10 | 11 | 12 | 13 | End2End Status 14 | 15 | 16 | 17 | Report Status 18 | 19 |

20 | 21 | GO4SQL is an open source project to write in-memory SQL engine using nothing but Golang. 22 | 23 | ## HOW TO USE 24 | 25 | You can compile the project with ``go build``, this will create ``GO4SQL`` binary. 26 | 27 | Currently, there are 3 modes to chose from: 28 | 29 | 1. `File Mode` - You can specify file path with ``./GO4SQL -file file_path``, that will read the 30 | input data directly into the program and print the result. In order to run one of e2e test files you can use: 31 | ```shell 32 | go build; ./GO4SQL -file e2e/test_files/1_select_with_where_test 33 | ``` 34 | 2. `Stream Mode` - With ``./GO4SQL -stream`` you can run the program in stream mode, then you 35 | provide SQL commands 36 | in your console (from standard input). 37 | 38 | 3. `Socket Mode` - To start Socket Server use `./GO4SQL -socket`, it will be listening on port 39 | `1433` by default. To 40 | choose port different other than that, for example equal to `1444`, go with: 41 | `./GO4SQL -socket -port 1444` 42 | 43 | ## UNIT TESTS 44 | 45 | To run all the tests locally paste this in root directory: 46 | 47 | ```shell 48 | go clean -testcache; go test ./... 49 | ``` 50 | 51 | ## E2E TESTS 52 | 53 | There are integrated with Github actions e2e tests that can be found in: `.github/workflows/end2end-tests.yml` file. 54 | Tests run files inside `e2e/test_files` directory through `GO4SQL`, save stdout into files, and finally compare 55 | then with expected outputs inside `e2e/expected_outputs` directory. 56 | 57 | To run e2e test locally, you can run script `./e2e/e2e_test.sh` if you're in the root directory. 58 | 59 | ## Docker 60 | 61 | 1. Pull docker image: `docker pull kajedot/go4sql:latest` 62 | 2. Run docker container in the interactive mode, remember to provide flag, for example: 63 | `docker run -i kajedot/go4sql -stream` 64 | 3. You can test this image with `test_file` provided in this repo: 65 | `docker run -i kajedot/go4sql -stream < test_file` 66 | 67 | ## SUPPORTED TYPES 68 | 69 | + **TEXT Type** - represents string values. Number or NULL can be converted to this type by wrapping 70 | with apostrophes. Columns can store this type with **TEXT** keyword while using **CREATE** 71 | command. 72 | + **NUMERIC Type** - represents integer values, columns can store this type with **INT** keyword 73 | while using **CREATE** command. In general every digit-only value is interpreted as this type. 74 | + **NULL Type** - columns can't be assigned that type, but it can be used with **INSERT INTO**, 75 | **UPDATE**, and inside **WHERE** statements, also it can be a product of **JOIN** commands 76 | (besides **FULL JOIN**). In GO4SQL NULL is the smallest possible value, what means it can be 77 | compared with other types with **EQUAL** and **NOT** statements. 78 | 79 | ## FUNCTIONALITY 80 | 81 | * ***CREATE TABLE*** - you can create table with name ``table1`` using 82 | command: 83 | ```sql 84 | CREATE TABLE table1( one TEXT , two INT); 85 | ``` 86 | First column is called ``one`` and it contains strings (keyword ``TEXT``), second 87 | one is called ``two`` and it contains integers (keyword ``INT``). 88 | 89 | * ***DROP TABLE*** - you can destroy the table of name ``table1`` using 90 | command: 91 | ```sql 92 | DROP TABLE table1; 93 | ``` 94 | After using this command table1 will no longer be available and all data connected to it (column 95 | definitions and inserted values) will be lost. 96 | 97 | 98 | * ***INSERT INTO*** - you can insert values into table called ``table1`` with 99 | command: 100 | ```sql 101 | INSERT INTO table1 VALUES( 'hello', 1); 102 | ``` 103 | Please note that the number of arguments and types of the values 104 | must be the same as you declared with ``CREATE``. 105 | 106 | * ***UPDATE*** - you can update values in table called ``table1`` with command: 107 | ```sql 108 | UPDATE table1 109 | SET column_name_1 TO new_value_1, column_name_2 TO new_value_2 110 | WHERE id EQUAL 1; 111 | ``` 112 | It will update all rows where column ``id`` is equal to ``1`` by replacing value in 113 | ``column_name_1`` with ``new_value_1`` and ``column_name_2`` with ``new_value_2``. 114 | 115 | * ***SELECT FROM*** - you can either select everything from ``table1`` with: 116 | ```SELECT * FROM table1;``` 117 | Or you can specify column names that you're interested in: 118 | ```sql 119 | SELECT one, two FROM table1; 120 | ``` 121 | Note that column names must be the 122 | same as you declared with ``CREATE`` and also duplicated column names will be ignored. 123 | 124 | 125 | * ***WHERE*** - is used to filter records. It is used to extract only those records that fulfill a 126 | specified condition. It can be used with ``SELECT`` like this: 127 | ```sql 128 | SELECT column1, column2 129 | FROM table_name 130 | WHERE column1 NOT 'goodbye' OR column2 EQUAL 3; 131 | ``` 132 | Supported logical operations are: ``EQUAL``, ``NOT``, ``OR``, ``AND``, ```FALSE```, ```TRUE```. 133 | 134 | * ***IN*** - is used to check if a value from a column exists in a specified list of values. 135 | It can be used with ``WHERE`` like this: 136 | ```sql 137 | SELECT column1, column2 138 | FROM table_name 139 | WHERE column1 IN ('value1', 'value2'); 140 | ``` 141 | ``table_name`` is the name of the table, and ``WHERE`` returns rows that value is either equal to 142 | ``value1`` or ``value2`` 143 | 144 | * ***NOTIN*** - is used to check if a value from a column doesn't exist in a specified list of 145 | values. It can be used with ``WHERE`` like this: 146 | ```sql 147 | SELECT column1, column2 148 | FROM table_name 149 | WHERE column1 NOTIN ('value1', 'value2'); 150 | ``` 151 | ``table_name`` is the name of the table, and ``WHERE`` returns rows which values are not equal to 152 | ``value1`` and not equal to ``value2`` 153 | 154 | * ***DELETE FROM*** is used to delete existing records in a table. It can be used like this: 155 | ```sql 156 | DELETE FROM tb1 WHERE two EQUAL 3; 157 | ``` 158 | ``tb1`` is the name of the table, and ``WHERE`` specify records that fulfill a 159 | specified condition and afterward will be deleted. 160 | 161 | 162 | * ***ORDER BY*** is used to sort the result-set in ascending or descending order. It can be used 163 | with ``SELECT`` like this: 164 | ```sql 165 | SELECT column1, column2, 166 | FROM table_name 167 | ORDER BY column1 ASC, column2 DESC; 168 | ``` 169 | In this case, this command will order by ``column1`` in ascending order, but if some rows have the 170 | same ``column1``, it orders them by column2 in descending order. 171 | 172 | * ***LIMIT*** is used to reduce number of rows printed out by returning only specified number of 173 | records with ``SELECT`` like this: 174 | ```sql 175 | SELECT column1, column2, 176 | FROM table_name 177 | ORDER BY column1 ASC 178 | LIMIT 5; 179 | ``` 180 | In this case, this command will order by ``column1`` in ascending order and return 5 first 181 | records. 182 | 183 | 184 | * ***OFFSET*** is used to reduce number of rows printed out by not skipping specified numbers of 185 | rows in returned output with ``SELECT`` like this: 186 | ```sql 187 | SELECT column1, column2, 188 | FROM table_name 189 | ORDER BY column1 ASC 190 | LIMIT 5 OFFSET 3; 191 | ``` 192 | In this case, this command will order by ``column1`` in ascending order and skip 3 first records, 193 | then return records from 4th to 8th. 194 | 195 | * ***DISTINCT*** is used to return only distinct (different) values in returned output with 196 | ``SELECT`` like this: 197 | ```sql 198 | SELECT DISTINCT column1, column2, 199 | FROM table_name; 200 | ``` 201 | In this case, this command will return only unique rows from ``table_name`` table. 202 | 203 | * ***INNER JOIN*** is used to return a new table by combining rows from both tables where there is a 204 | match on the 205 | specified condition. Only the rows that satisfy the condition from both tables are included in the 206 | result. 207 | Rows from either table that do not meet the condition are excluded from the result. 208 | ```sql 209 | SELECT * 210 | FROM tableOne 211 | JOIN tableTwo 212 | ON tableOne.columnY EQUAL tableTwo.columnX; 213 | ``` 214 | or 215 | ```sql 216 | SELECT * 217 | FROM tableOne 218 | INNER JOIN tableTwo 219 | ON tableOne.columnY EQUAL tableTwo.columnX; 220 | ``` 221 | In this case, this command will return all columns from tableOne and tableTwo for rows where the 222 | condition 223 | ``tableOne.columnY`` = ``tableTwo.columnX`` is met (i.e., the value of ``columnY`` in ``tableOne`` 224 | is equal to the 225 | value of ``columnX`` in ``tableTwo``). 226 | * ***LEFT JOIN*** is used to return a new table that includes all records from the left table and 227 | the matched records 228 | from the right table. If there is no match, the result will contain empty values for columns from 229 | the right table. 230 | ```sql 231 | SELECT * 232 | FROM tableOne 233 | LEFT JOIN tableTwo 234 | ON tableOne.columnY EQUAL tableTwo.columnX; 235 | ``` 236 | In this case, this command will return all columns from ``tableOne`` and the matching columns from 237 | ``tableTwo``. For 238 | rows in 239 | ``tableOne`` that do not have a corresponding match in ``tableTwo``, the result will include empty 240 | values for columns 241 | from 242 | ``tableTwo``. 243 | * ***RIGHT JOIN*** is used to return a new table that includes all records from the right table and 244 | the matched records 245 | from the left table. If there is no match, the result will contain empty values for columns from 246 | the left table. 247 | ```sql 248 | SELECT * 249 | FROM tableOne 250 | RIGHT JOIN tableTwo 251 | ON tableOne.columnY EQUAL tableTwo.columnX; 252 | ``` 253 | In this case, this command will return all columns from ``tableTwo`` and the matching columns from 254 | ``tableOne``. For 255 | rows in 256 | ``tableTwo`` that do not have a corresponding match in ``tableOne``, the result will include empty 257 | values for columns 258 | from 259 | ``tableOne``. 260 | 261 | * ***FULL JOIN*** is used to return a new table created by joining two tables as a whole. The 262 | joined table contains all 263 | records from both tables and fills empty values for missing matches on either side. This join 264 | combines the results of 265 | both ``LEFT JOIN`` and ``RIGHT JOIN``. 266 | ```sql 267 | SELECT * 268 | FROM tableOne 269 | FULL JOIN tableTwo 270 | ON tableOne.columnY EQUAL tableTwo.columnX; 271 | ``` 272 | In this case, this command will return all columns from ``tableOne`` and ``tableTwo`` for rows 273 | fulfilling condition 274 | ``tableOne.columnY EQUAL tableTwo.columnX`` (value of ``columnY`` in ``tableOne`` is equal the 275 | value of ``columnX`` in 276 | ``tableTwo``). 277 | 278 | * ***MIN()*** is used to return the smallest value in a specified column. 279 | ```sql 280 | SELECT MIN(columnName) 281 | FROM tableName; 282 | ``` 283 | In this case, this command will return the smallest value found in the column ``columnName`` of 284 | ``tableName``. 285 | 286 | * ***MAX()*** is used to return the largest value in a specified column. 287 | ```sql 288 | SELECT MAX(columnName) 289 | FROM tableName; 290 | ``` 291 | This command will return the largest value found in the column ``columnName`` of ``tableName``. 292 | 293 | * ***COUNT()*** is used to return the number of rows that match a given condition or the total 294 | number of rows in a 295 | specified column. 296 | ```sql 297 | SELECT COUNT(columnName) 298 | FROM tableName; 299 | ``` 300 | This command will return the number of rows in the ``columnName`` of ``tableName``. 301 | 302 | * ***SUM()*** is used to return the total sum of the values in a specified numerical column. 303 | ```sql 304 | SELECT SUM(columnName) 305 | FROM tableName; 306 | ``` 307 | This command will return the total sum of all values in the numerical column ``columnName`` of 308 | ``tableName``. 309 | 310 | * ***AVG()*** is used to return the average of values in a specified numerical column. 311 | ```sql 312 | SELECT AVG(columnName) 313 | FROM tableName; 314 | ``` 315 | This command will return the average of all values in the numerical column ``columnName`` of 316 | ``tableName``. 317 | 318 | ## DOCKER 319 | 320 | To build your docker image run this command in root directory: 321 | 322 | ```shell 323 | docker build -t go4sql:test . 324 | ``` 325 | 326 | ### Run docker in interactive stream mode 327 | 328 | To run this docker image in interactive stream mode use this command: 329 | 330 | ```shell 331 | docker run -i go4sql:test -stream 332 | ``` 333 | 334 | ### Run docker in socket mode 335 | 336 | To run this docker image in socket mode use this command: 337 | 338 | ```shell 339 | docker run go4sql:test -socket 340 | ``` 341 | 342 | ### Run docker in file mode 343 | 344 | **NOT RECOMMENDED** 345 | 346 | Alternatively you can run a docker image in file mode: 347 | 348 | ```shell 349 | docker run -i go4sql:test -file 350 | ``` 351 | 352 | ## HELM 353 | 354 | To create a pod deployment using helm chart, there is configuration under `./helm` directory. 355 | 356 | Commands: 357 | 358 | ```shell 359 | cd ./helm 360 | helm install go4sql_pod_name GO4SQL/ 361 | ``` 362 | 363 | To check status of pod, use: 364 | 365 | ```shell 366 | kubectl get pods 367 | ``` 368 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "github.com/LissaGreense/GO4SQL/token" 4 | 5 | // Sequence - Sequence of operations commands 6 | // 7 | // Example: 8 | // Commands[0] = SELECT * FROM Customers 9 | // Commands[1] = WHERE City LIKE '%es%'; 10 | type Sequence struct { 11 | Commands []Command 12 | } 13 | 14 | // Node is a connector between commands and expressions 15 | type Node interface { 16 | TokenLiteral() string 17 | } 18 | 19 | // Command - Part of sequence - represent single static command 20 | // 21 | // Example: 22 | // SELECT * FROM Customers; 23 | type Command interface { 24 | Node 25 | CommandNode() 26 | } 27 | 28 | // Expression - Mathematical expression that is used to evaluate conditions 29 | // 30 | // Methods: 31 | // 32 | // GetIdentifiers - Return array for all Identifiers within expression 33 | type Expression interface { 34 | GetIdentifiers() []Identifier 35 | } 36 | 37 | // Tifier - Interface that represent Token with string value 38 | // 39 | // Methods: 40 | // 41 | // IsIdentifier - Check if Tifier is Identifier 42 | // GetToken - return token within Tifier 43 | type Tifier interface { 44 | IsIdentifier() bool 45 | GetToken() token.Token 46 | } 47 | 48 | // TokenLiteral - Return first literal in sequence 49 | func (p *Sequence) TokenLiteral() string { 50 | if len(p.Commands) > 0 { 51 | return p.Commands[0].TokenLiteral() 52 | } 53 | return "" 54 | } 55 | 56 | // Identifier - Represent Token with string value that is equal to either column or table name 57 | type Identifier struct { 58 | Token token.Token // the token.IDENT token 59 | } 60 | 61 | func (ls Identifier) IsIdentifier() bool { return true } 62 | func (ls Identifier) GetToken() token.Token { return ls.Token } 63 | 64 | // Anonymitifier - Represent Token with a string value that is equal to a simple value that is put into columns 65 | type Anonymitifier struct { 66 | Token token.Token // the token.IDENT token 67 | } 68 | 69 | func (ls Anonymitifier) IsIdentifier() bool { return false } 70 | func (ls Anonymitifier) GetToken() token.Token { return ls.Token } 71 | 72 | // BooleanExpression - TokenType of Expression that represent single boolean value 73 | // 74 | // Example: 75 | // TRUE 76 | type BooleanExpression struct { 77 | Boolean token.Token // example: token.TRUE 78 | } 79 | 80 | func (ls BooleanExpression) GetIdentifiers() []Identifier { 81 | var identifiers []Identifier 82 | return identifiers 83 | } 84 | 85 | // ConditionExpression - TokenType of Expression that represent condition that is comparing value from column to static one 86 | // 87 | // Example: 88 | // column1 EQUAL 123 89 | type ConditionExpression struct { 90 | Left Tifier // name of column 91 | Right Tifier // value which column should have 92 | Condition token.Token // example: token.EQUAL 93 | } 94 | 95 | func (ls ConditionExpression) GetIdentifiers() []Identifier { 96 | var identifiers []Identifier 97 | 98 | if ls.Left.IsIdentifier() { 99 | identifiers = append(identifiers, Identifier{ls.Left.GetToken()}) 100 | } 101 | 102 | if ls.Right.IsIdentifier() { 103 | identifiers = append(identifiers, Identifier{ls.Right.GetToken()}) 104 | } 105 | 106 | return identifiers 107 | } 108 | 109 | // ContainExpression - TokenType of Expression that represents structure for-IN-operator 110 | // 111 | // Example: 112 | // colName IN ('value1', 'value2', 'value3') 113 | type ContainExpression struct { 114 | Left Identifier // name of column 115 | Right []Anonymitifier // name of column 116 | Contains bool // IN or NOTIN 117 | } 118 | 119 | func (ls ContainExpression) GetIdentifiers() []Identifier { 120 | return []Identifier{ls.Left} 121 | } 122 | 123 | // OperationExpression - TokenType of Expression that represent 2 other Expressions and conditional operation 124 | // 125 | // Example: 126 | // TRUE OR FALSE 127 | type OperationExpression struct { 128 | Left Expression // another operation, condition or boolean 129 | Right Expression // another operation, condition or boolean 130 | Operation token.Token // example: token.AND 131 | } 132 | 133 | func (ls OperationExpression) GetIdentifiers() []Identifier { 134 | var identifiers []Identifier 135 | 136 | identifiers = append(identifiers, ls.Left.GetIdentifiers()...) 137 | identifiers = append(identifiers, ls.Right.GetIdentifiers()...) 138 | 139 | return identifiers 140 | } 141 | 142 | // CreateCommand - Part of Command that represent creation of table 143 | // 144 | // Example: 145 | // CREATE TABLE table1( one TEXT , two INT); 146 | type CreateCommand struct { 147 | Token token.Token 148 | Name Identifier // name of the table 149 | ColumnNames []string 150 | ColumnTypes []token.Token 151 | } 152 | 153 | func (ls CreateCommand) CommandNode() {} 154 | func (ls CreateCommand) TokenLiteral() string { return ls.Token.Literal } 155 | 156 | // InsertCommand - Part of Command that represent insertion of values into columns 157 | // 158 | // Example: 159 | // INSERT INTO table1 VALUES('hello', 1); 160 | type InsertCommand struct { 161 | Token token.Token 162 | Name Identifier // name of the table 163 | Values []token.Token 164 | } 165 | 166 | func (ls InsertCommand) CommandNode() {} 167 | func (ls InsertCommand) TokenLiteral() string { return ls.Token.Literal } 168 | 169 | // Space - part of SelectCommand which is containing either * or a column name with an optional function aggregating it 170 | type Space struct { 171 | ColumnName token.Token 172 | AggregateFunc *token.Token 173 | } 174 | 175 | func (space Space) String() string { 176 | columnName := "ColumnName={Type: " + string(space.ColumnName.Type) + ", Literal: " + space.ColumnName.Literal + "}" 177 | if space.ContainsAggregateFunc() { 178 | aggFunc := "AggregateFunc={Type: " + string(space.AggregateFunc.Type) + ", Literal: " + space.AggregateFunc.Literal + "}" 179 | return columnName + ", " + aggFunc 180 | } 181 | return columnName 182 | } 183 | 184 | // ContainsAggregateFunc - return true if space contains AggregateFunc that aggregate columnName or * 185 | func (space Space) ContainsAggregateFunc() bool { 186 | return space.AggregateFunc != nil 187 | } 188 | 189 | // SelectCommand - Part of Command that represent selecting values from tables 190 | // 191 | // Example: 192 | // SELECT one, two FROM table1; 193 | type SelectCommand struct { 194 | Token token.Token 195 | Name Identifier // ex. name of table 196 | Space []Space // ex. column names 197 | HasDistinct bool // DISTINCT keyword has been used 198 | WhereCommand *WhereCommand // optional 199 | OrderByCommand *OrderByCommand // optional 200 | LimitCommand *LimitCommand // optional 201 | OffsetCommand *OffsetCommand // optional 202 | JoinCommand *JoinCommand // optional 203 | } 204 | 205 | func (ls SelectCommand) CommandNode() {} 206 | func (ls SelectCommand) TokenLiteral() string { return ls.Token.Literal } 207 | func (ls SelectCommand) AggregateFunctionAppears() bool { 208 | for _, space := range ls.Space { 209 | if space.ContainsAggregateFunc() { 210 | return true 211 | } 212 | } 213 | return false 214 | } 215 | 216 | // HasWhereCommand - returns true if optional HasWhereCommand is present in SelectCommand 217 | // 218 | // Example: 219 | // SELECT * FROM table WHERE column1 NOT 'hi'; 220 | // Returns true 221 | // 222 | // SELECT * FROM table; 223 | // Returns false 224 | func (ls SelectCommand) HasWhereCommand() bool { 225 | return ls.WhereCommand != nil 226 | } 227 | 228 | // HasOrderByCommand - returns true if optional OrderByCommand is present in SelectCommand 229 | // 230 | // Example: 231 | // SELECT * FROM table ORDER BY column1 ASC; 232 | // Returns true 233 | // 234 | // SELECT * FROM table; 235 | // Returns false 236 | func (ls SelectCommand) HasOrderByCommand() bool { 237 | return ls.OrderByCommand != nil 238 | } 239 | 240 | // HasLimitCommand - returns true if optional LimitCommand is present in SelectCommand 241 | // 242 | // Example: 243 | // SELECT * FROM table LIMIT 5; 244 | // Returns true 245 | // 246 | // SELECT * FROM table; 247 | // Returns false 248 | func (ls SelectCommand) HasLimitCommand() bool { 249 | return ls.LimitCommand != nil 250 | } 251 | 252 | // HasOffsetCommand - returns true if optional OffsetCommand is present in SelectCommand 253 | // 254 | // Example: 255 | // SELECT * FROM table OFFSET 100; 256 | // Returns true 257 | // 258 | // SELECT * FROM table LIMIT 10; 259 | // Returns false 260 | func (ls SelectCommand) HasOffsetCommand() bool { 261 | return ls.OffsetCommand != nil 262 | } 263 | 264 | // HasJoinCommand - returns true if optional JoinCommand is present in SelectCommand 265 | // 266 | // Example: 267 | // SELECT * FROM table JOIN table2 ON table.one EQUAL table2.two; 268 | // Returns true 269 | // 270 | // SELECT * FROM table; 271 | // Returns false 272 | func (ls SelectCommand) HasJoinCommand() bool { 273 | return ls.JoinCommand != nil 274 | } 275 | 276 | // UpdateCommand - Part of Command that allow to change existing data 277 | // 278 | // Example: 279 | // UPDATE table SET col1 TO 2 WHERE column1 NOT 'hi'; 280 | type UpdateCommand struct { 281 | Token token.Token 282 | Name Identifier // ex. name of table 283 | Changes map[token.Token]Anonymitifier // column names with new values 284 | WhereCommand *WhereCommand // optional 285 | } 286 | 287 | func (ls UpdateCommand) CommandNode() {} 288 | func (ls UpdateCommand) TokenLiteral() string { return ls.Token.Literal } 289 | 290 | // HasWhereCommand - returns true if optional HasWhereCommand is present in UpdateCommand 291 | // 292 | // Example: 293 | // UPDATE table SET col1 TO 2 WHERE column1 NOT 'hi'; 294 | // Returns true 295 | // 296 | // UPDATE table SET col1 TO 2; 297 | // Returns false 298 | func (ls UpdateCommand) HasWhereCommand() bool { 299 | if ls.WhereCommand == nil { 300 | return false 301 | } 302 | return true 303 | } 304 | 305 | // WhereCommand - Part of Command that represent Where statement with expression that will qualify values from Select 306 | // 307 | // Example: 308 | // WHERE column1 NOT 'goodbye' OR column2 EQUAL 3; 309 | type WhereCommand struct { 310 | Token token.Token 311 | Expression Expression 312 | } 313 | 314 | func (ls WhereCommand) CommandNode() {} 315 | func (ls WhereCommand) TokenLiteral() string { return ls.Token.Literal } 316 | 317 | // JoinCommand - Part of Command that represent JOIN statement with expression that will merge tables 318 | // 319 | // Example: 320 | // JOIN tbl2 ON tbl1.id EQUAL tbl2.f_idy; 321 | type JoinCommand struct { 322 | Token token.Token 323 | Name Identifier // ex. name of table 324 | JoinType token.Token 325 | Expression Expression 326 | } 327 | 328 | func (ls JoinCommand) CommandNode() {} 329 | func (ls JoinCommand) TokenLiteral() string { return ls.Token.Literal } 330 | func (ls JoinCommand) ShouldTakeLeftSide() bool { 331 | return ls.JoinType.Type == token.LEFT || ls.JoinType.Type == token.FULL 332 | } 333 | func (ls JoinCommand) ShouldTakeRightSide() bool { 334 | return ls.JoinType.Type == token.RIGHT || ls.JoinType.Type == token.FULL 335 | } 336 | 337 | // DeleteCommand - Part of Command that represent deleting row from table 338 | // 339 | // Example: 340 | // DELETE FROM tb1 WHERE two EQUAL 3; 341 | type DeleteCommand struct { 342 | Token token.Token 343 | Name Identifier // name of the table 344 | WhereCommand *WhereCommand // optional 345 | } 346 | 347 | func (ls DeleteCommand) CommandNode() {} 348 | func (ls DeleteCommand) TokenLiteral() string { return ls.Token.Literal } 349 | 350 | // DropCommand - Part of Command that represent dropping table 351 | // 352 | // Example: 353 | // DROP TABLE table; 354 | type DropCommand struct { 355 | Token token.Token 356 | Name Identifier // name of the table 357 | } 358 | 359 | func (ls DropCommand) CommandNode() {} 360 | func (ls DropCommand) TokenLiteral() string { return ls.Token.Literal } 361 | 362 | // HasWhereCommand - returns true if optional HasWhereCommand is present in SelectCommand 363 | // 364 | // Example: 365 | // SELECT * FROM table WHERE column1 NOT 'hi'; 366 | // Returns true 367 | // 368 | // SELECT * FROM table; 369 | // Returns false 370 | func (ls DeleteCommand) HasWhereCommand() bool { 371 | if ls.WhereCommand == nil { 372 | return false 373 | } 374 | return true 375 | } 376 | 377 | // OrderByCommand - Part of Command that ordering columns from SelectCommand 378 | // 379 | // Example: 380 | // ORDER BY column1 ASC, column2 DESC; 381 | type OrderByCommand struct { 382 | Token token.Token 383 | SortPatterns []SortPattern // column name and sorting type 384 | } 385 | 386 | func (ls OrderByCommand) CommandNode() {} 387 | func (ls OrderByCommand) TokenLiteral() string { return ls.Token.Literal } 388 | 389 | // SortPattern - Represent in which order declared columns should be sorted 390 | type SortPattern struct { 391 | ColumnName token.Token // column name 392 | Order token.Token // ASC or DESC 393 | } 394 | 395 | // LimitCommand - Part of Command that limits results from SelectCommand 396 | type LimitCommand struct { 397 | Token token.Token 398 | Count int 399 | } 400 | 401 | func (ls LimitCommand) CommandNode() {} 402 | func (ls LimitCommand) TokenLiteral() string { return ls.Token.Literal } 403 | 404 | // OffsetCommand - Part of Command that skip begging rows from SelectCommand 405 | type OffsetCommand struct { 406 | Token token.Token 407 | Count int 408 | } 409 | 410 | func (ls OffsetCommand) CommandNode() {} 411 | func (ls OffsetCommand) TokenLiteral() string { return ls.Token.Literal } 412 | -------------------------------------------------------------------------------- /ast/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ast (Abstract Syntax Tree) is a tree representation of the abstract synthetic structure of go4sql. 3 | */ 4 | package ast 5 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | GO4SQL is in-memory database. Syntax is basing on ISO/IEC SC 32 standard. 3 | 4 | Usage: 5 | 6 | GO4SQL [flags] 7 | 8 | Flags: 9 | 10 | -file PATH 11 | Get provided .sql file and read data directly into the program. 12 | -stream 13 | Use to redirect stdin to stdout 14 | */ 15 | package main 16 | -------------------------------------------------------------------------------- /e2e/e2e_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | e2e_failed=false 4 | 5 | for test_file in e2e/test_files/*_test; do 6 | output_file="./e2e/$(basename "${test_file/_test/_output}")" 7 | ./GO4SQL -file "$test_file" > "$output_file" 8 | expected_output="e2e/expected_outputs/$(basename "${test_file/_test/_expected_output}")" 9 | diff "$output_file" "$expected_output" 10 | if [ $? -ne 0 ]; then 11 | echo "E2E test for: {$test_file} failed" 12 | e2e_failed=true 13 | fi 14 | rm "./$output_file" 15 | done 16 | 17 | if [ "$e2e_failed" = true ]; then 18 | echo "E2E tests failed." 19 | exit 1 20 | else 21 | echo "All E2E tests passed." 22 | fi 23 | -------------------------------------------------------------------------------- /e2e/expected_outputs/1_select_with_where_expected_output: -------------------------------------------------------------------------------- 1 | Table 'tbl' has been created 2 | Data Inserted 3 | Data Inserted 4 | Data Inserted 5 | +----------+------+-------+------+ 6 | | one | two | three | four | 7 | +----------+------+-------+------+ 8 | | 'byebye' | NULL | 33 | 'e' | 9 | +----------+------+-------+------+ 10 | +-----------+-------+ 11 | | one | three | 12 | +-----------+-------+ 13 | | 'hello' | 11 | 14 | | 'goodbye' | 22 | 15 | +-----------+-------+ 16 | +----------+------+-------+------+ 17 | | one | two | three | four | 18 | +----------+------+-------+------+ 19 | | 'byebye' | NULL | 33 | 'e' | 20 | +----------+------+-------+------+ 21 | +-----------+------+-------+------+ 22 | | one | two | three | four | 23 | +-----------+------+-------+------+ 24 | | 'goodbye' | 1 | 22 | 'w' | 25 | | 'byebye' | NULL | 33 | 'e' | 26 | +-----------+------+-------+------+ 27 | +---------+-----+-------+------+ 28 | | one | two | three | four | 29 | +---------+-----+-------+------+ 30 | | 'hello' | 1 | 11 | 'q' | 31 | +---------+-----+-------+------+ 32 | +-----+-----+-------+------+ 33 | | one | two | three | four | 34 | +-----+-----+-------+------+ 35 | +-----+-----+-------+------+ 36 | +-----------+------+-------+------+ 37 | | one | two | three | four | 38 | +-----------+------+-------+------+ 39 | | 'hello' | 1 | 11 | 'q' | 40 | | 'goodbye' | 1 | 22 | 'w' | 41 | | 'byebye' | NULL | 33 | 'e' | 42 | +-----------+------+-------+------+ 43 | -------------------------------------------------------------------------------- /e2e/expected_outputs/2_select_with_limit_and_offset_expected_output: -------------------------------------------------------------------------------- 1 | Table 'tbl' has been created 2 | Data Inserted 3 | Data Inserted 4 | Data Inserted 5 | +---------+-----+-------+------+ 6 | | one | two | three | four | 7 | +---------+-----+-------+------+ 8 | | 'hello' | 1 | 11 | 'q' | 9 | +---------+-----+-------+------+ 10 | +-----------+------+-------+------+ 11 | | one | two | three | four | 12 | +-----------+------+-------+------+ 13 | | 'goodbye' | 1 | 22 | 'w' | 14 | | 'byebye' | NULL | 33 | 'e' | 15 | +-----------+------+-------+------+ 16 | +-----------+-----+-------+------+ 17 | | one | two | three | four | 18 | +-----------+-----+-------+------+ 19 | | 'goodbye' | 1 | 22 | 'w' | 20 | +-----------+-----+-------+------+ 21 | -------------------------------------------------------------------------------- /e2e/expected_outputs/3_delete_expected_output: -------------------------------------------------------------------------------- 1 | Table 'tbl' has been created 2 | Data Inserted 3 | Data Inserted 4 | Data Inserted 5 | Data from 'tbl' has been deleted 6 | +-----------+-----+-------+------+ 7 | | one | two | three | four | 8 | +-----------+-----+-------+------+ 9 | | 'hello' | 1 | 11 | 'q' | 10 | | 'goodbye' | 1 | 22 | 'w' | 11 | +-----------+-----+-------+------+ 12 | -------------------------------------------------------------------------------- /e2e/expected_outputs/4_orderby_expected_output: -------------------------------------------------------------------------------- 1 | Table 'tbl' has been created 2 | Data Inserted 3 | Data Inserted 4 | Data Inserted 5 | +-----------+ 6 | | one | 7 | +-----------+ 8 | | 'byebye' | 9 | | 'goodbye' | 10 | | 'hello' | 11 | +-----------+ 12 | -------------------------------------------------------------------------------- /e2e/expected_outputs/5_update_expected_output: -------------------------------------------------------------------------------- 1 | Table 'tbl' has been created 2 | Data Inserted 3 | Data Inserted 4 | Data Inserted 5 | Table: 'tbl' has been updated 6 | +-----------+------+-------+------+ 7 | | one | two | three | four | 8 | +-----------+------+-------+------+ 9 | | 'hello' | 1 | 11 | 'q' | 10 | | 'goodbye' | NULL | 22 | 'P' | 11 | | 'byebye' | NULL | 33 | 'e' | 12 | +-----------+------+-------+------+ 13 | -------------------------------------------------------------------------------- /e2e/expected_outputs/6_select_distinct_expected_output: -------------------------------------------------------------------------------- 1 | Table 'tbl' has been created 2 | Data Inserted 3 | Data Inserted 4 | Data Inserted 5 | Data Inserted 6 | Data Inserted 7 | +-----------+------+-------+------+ 8 | | one | two | three | four | 9 | +-----------+------+-------+------+ 10 | | 'hello' | 1 | 11 | 'q' | 11 | | 'goodbye' | 1 | 22 | 'w' | 12 | | 'byebye' | NULL | 33 | 'e' | 13 | +-----------+------+-------+------+ 14 | -------------------------------------------------------------------------------- /e2e/expected_outputs/7_drop_table_expected_output: -------------------------------------------------------------------------------- 1 | Table 'tbl' has been created 2 | Table: 'tbl' has been dropped 3 | -------------------------------------------------------------------------------- /e2e/expected_outputs/8_select_with_join_expected_output: -------------------------------------------------------------------------------- 1 | Table 'table1' has been created 2 | Table 'table2' has been created 3 | Data Inserted 4 | Data Inserted 5 | Data Inserted 6 | Data Inserted 7 | +--------------+--------------+ 8 | | table1.value | table2.value | 9 | +--------------+--------------+ 10 | | 'Value1' | NULL | 11 | | NULL | 'Value2' | 12 | | NULL | 'Value3' | 13 | +--------------+--------------+ 14 | +--------------+--------------+ 15 | | table1.value | table2.value | 16 | +--------------+--------------+ 17 | | NULL | 'Value2' | 18 | +--------------+--------------+ 19 | +--------------+--------------+ 20 | | table1.value | table2.value | 21 | +--------------+--------------+ 22 | | 'Value1' | NULL | 23 | | NULL | 'Value2' | 24 | +--------------+--------------+ 25 | +--------------+--------------+ 26 | | table1.value | table2.value | 27 | +--------------+--------------+ 28 | | NULL | 'Value2' | 29 | | NULL | 'Value3' | 30 | +--------------+--------------+ 31 | -------------------------------------------------------------------------------- /e2e/expected_outputs/9_aggregate_functions_expected_output: -------------------------------------------------------------------------------- 1 | Table 'table1' has been created 2 | Table 'table2' has been created 3 | Data Inserted 4 | Data Inserted 5 | Data Inserted 6 | Data Inserted 7 | +---------+------------+ 8 | | MAX(id) | MAX(value) | 9 | +---------+------------+ 10 | | 2 | Value1 | 11 | +---------+------------+ 12 | +------------+---------+ 13 | | MIN(value) | MIN(id) | 14 | +------------+---------+ 15 | | NULL | 1 | 16 | +------------+---------+ 17 | +----------+-----------+--------------+ 18 | | COUNT(*) | COUNT(id) | COUNT(value) | 19 | +----------+-----------+--------------+ 20 | | 2 | 2 | 1 | 21 | +----------+-----------+--------------+ 22 | +---------+------------+ 23 | | SUM(id) | SUM(value) | 24 | +---------+------------+ 25 | | 3 | 0 | 26 | +---------+------------+ 27 | +---------+------------+ 28 | | AVG(id) | AVG(value) | 29 | +---------+------------+ 30 | | 1 | 0 | 31 | +---------+------------+ 32 | +---------+----+ 33 | | AVG(id) | id | 34 | +---------+----+ 35 | | 1 | 1 | 36 | +---------+----+ 37 | -------------------------------------------------------------------------------- /e2e/test_files/1_select_with_where_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE tbl( one TEXT , two INT, three INT, four TEXT ); 2 | 3 | INSERT INTO tbl VALUES( 'hello',1, 11, 'q' ); 4 | INSERT INTO tbl VALUES( 'goodbye', 1, 22, 'w' ); 5 | INSERT INTO tbl VALUES( 'byebye', NULL, 33,'e' ); 6 | 7 | SELECT * FROM tbl WHERE one EQUAL 'byebye'; 8 | SELECT one, three FROM tbl WHERE two NOT NULL; 9 | SELECT * FROM tbl WHERE one NOT 'goodbye' AND two EQUAL NULL; 10 | SELECT * FROM tbl WHERE one IN ('goodbye', 'byebye'); 11 | SELECT * FROM tbl WHERE one NOTIN ('goodbye', 'byebye'); 12 | SELECT * FROM tbl WHERE FALSE; 13 | SELECT * FROM tbl WHERE 'colName1 EQUAL;' EQUAL 'colName1 EQUAL;'; 14 | -------------------------------------------------------------------------------- /e2e/test_files/2_select_with_limit_and_offset_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE tbl( one TEXT , two INT, three INT, four TEXT ); 2 | 3 | INSERT INTO tbl VALUES( 'hello',1, 11, 'q' ); 4 | INSERT INTO tbl VALUES( 'goodbye', 1, 22, 'w' ); 5 | INSERT INTO tbl VALUES( 'byebye', NULL, 33,'e' ); 6 | 7 | SELECT * FROM tbl LIMIT 1; 8 | SELECT * FROM tbl OFFSET 1; 9 | SELECT * FROM tbl LIMIT 1 OFFSET 1; 10 | -------------------------------------------------------------------------------- /e2e/test_files/3_delete_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE tbl( one TEXT , two INT, three INT, four TEXT ); 2 | 3 | INSERT INTO tbl VALUES( 'hello',1, 11, 'q' ); 4 | INSERT INTO tbl VALUES( 'goodbye', 1, 22, 'w' ); 5 | INSERT INTO tbl VALUES( 'byebye', NULL, 33,'e' ); 6 | 7 | DELETE FROM tbl WHERE one EQUAL 'byebye'; 8 | 9 | SELECT * FROM tbl; 10 | -------------------------------------------------------------------------------- /e2e/test_files/4_orderby_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE tbl( one TEXT , two INT, three INT, four TEXT ); 2 | 3 | INSERT INTO tbl VALUES( 'hello',1, 11, 'q' ); 4 | INSERT INTO tbl VALUES( 'goodbye', 1, 22, 'w' ); 5 | INSERT INTO tbl VALUES( 'byebye', NULL, 33,'e' ); 6 | 7 | SELECT one FROM tbl WHERE TRUE ORDER BY two ASC, four DESC; 8 | 9 | -------------------------------------------------------------------------------- /e2e/test_files/5_update_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE tbl( one TEXT , two INT, three INT, four TEXT ); 2 | 3 | INSERT INTO tbl VALUES( 'hello',1, 11, 'q' ); 4 | INSERT INTO tbl VALUES( 'goodbye', 1, 22, 'w' ); 5 | INSERT INTO tbl VALUES( 'byebye', NULL, 33,'e' ); 6 | 7 | UPDATE tbl SET two TO NULL, four TO 'P' WHERE one EQUAL 'goodbye'; 8 | 9 | SELECT * FROM tbl; 10 | -------------------------------------------------------------------------------- /e2e/test_files/6_select_distinct_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE tbl( one TEXT , two INT, three INT, four TEXT ); 2 | 3 | INSERT INTO tbl VALUES( 'hello',1, 11, 'q' ); 4 | INSERT INTO tbl VALUES( 'goodbye', 1, 22, 'w' ); 5 | INSERT INTO tbl VALUES( 'byebye', NULL, 33,'e' ); 6 | INSERT INTO tbl VALUES( 'goodbye', 1, 22, 'w' ); 7 | INSERT INTO tbl VALUES( 'byebye', NULL, 33,'e' ); 8 | 9 | SELECT DISTINCT * FROM tbl; 10 | -------------------------------------------------------------------------------- /e2e/test_files/7_drop_table_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE tbl( one TEXT , two INT, three INT, four TEXT ); 2 | 3 | DROP TABLE tbl; 4 | -------------------------------------------------------------------------------- /e2e/test_files/8_select_with_join_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE table1( id INT, value TEXT); 2 | CREATE TABLE table2( id INT, value TEXT); 3 | 4 | INSERT INTO table1 VALUES(1, 'Value1'); 5 | INSERT INTO table1 VALUES(2, NULL); 6 | INSERT INTO table2 VALUES(2, 'Value2'); 7 | INSERT INTO table2 VALUES(3, 'Value3'); 8 | 9 | SELECT table1.value, table2.value FROM table1 FULL JOIN table2 ON table1.id EQUAL table2.id; 10 | SELECT table1.value, table2.value FROM table1 INNER JOIN table2 ON table1.id EQUAL table2.id; 11 | SELECT table1.value, table2.value FROM table1 LEFT JOIN table2 ON table1.id EQUAL table2.id; 12 | SELECT table1.value, table2.value FROM table1 RIGHT JOIN table2 ON table1.id EQUAL table2.id; 13 | -------------------------------------------------------------------------------- /e2e/test_files/9_aggregate_functions_test: -------------------------------------------------------------------------------- 1 | CREATE TABLE table1( id INT, value TEXT); 2 | CREATE TABLE table2( id INT, value TEXT); 3 | 4 | INSERT INTO table1 VALUES(1, 'Value1'); 5 | INSERT INTO table1 VALUES(2, NULL); 6 | INSERT INTO table2 VALUES(2, 'Value2'); 7 | INSERT INTO table2 VALUES(3, 'Value3'); 8 | 9 | SELECT MAX(id), MAX(value) FROM table1; 10 | SELECT MIN(value), MIN(id) FROM table1; 11 | SELECT COUNT(*), COUNT(id), COUNT(value) FROM table1; 12 | SELECT SUM(id), SUM(value) FROM table1; 13 | SELECT AVG(id), AVG(value) FROM table1; 14 | SELECT AVG(id), id FROM table1; 15 | -------------------------------------------------------------------------------- /engine/column.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/LissaGreense/GO4SQL/token" 5 | ) 6 | 7 | // Column - part of the Table containing the name of Column and values in it 8 | type Column struct { 9 | Name string 10 | Type token.Token 11 | Values []ValueInterface 12 | } 13 | 14 | func extractColumnContent(columns []*Column, wantedColumnNames *[]string, tableName string) (*Table, error) { 15 | selectedTable := &Table{Columns: make([]*Column, 0)} 16 | mappedIndexes := make([]int, 0) 17 | for wantedColumnIndex := range *wantedColumnNames { 18 | for columnNameIndex := range columns { 19 | if columns[columnNameIndex].Name == (*wantedColumnNames)[wantedColumnIndex] { 20 | mappedIndexes = append(mappedIndexes, columnNameIndex) 21 | break 22 | } 23 | if columnNameIndex == len(columns)-1 { 24 | return nil, &ColumnDoesNotExistError{columnName: (*wantedColumnNames)[wantedColumnIndex], tableName: tableName} 25 | } 26 | } 27 | } 28 | 29 | for i := range mappedIndexes { 30 | selectedTable.Columns = append(selectedTable.Columns, &Column{ 31 | Name: columns[mappedIndexes[i]].Name, 32 | Type: columns[mappedIndexes[i]].Type, 33 | Values: make([]ValueInterface, 0), 34 | }) 35 | } 36 | if len(columns) == 0 { 37 | return selectedTable, nil 38 | } 39 | 40 | rowsCount := len(columns[0].Values) 41 | 42 | for iRow := 0; iRow < rowsCount; iRow++ { 43 | for iColumn := 0; iColumn < len(mappedIndexes); iColumn++ { 44 | selectedTable.Columns[iColumn].Values = 45 | append(selectedTable.Columns[iColumn].Values, columns[mappedIndexes[iColumn]].Values[iRow]) 46 | } 47 | } 48 | return selectedTable, nil 49 | } 50 | -------------------------------------------------------------------------------- /engine/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package engine executes logic operations on sequences. 3 | */ 4 | package engine 5 | -------------------------------------------------------------------------------- /engine/engine.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "fmt" 5 | "maps" 6 | "sort" 7 | "strconv" 8 | 9 | "github.com/LissaGreense/GO4SQL/ast" 10 | "github.com/LissaGreense/GO4SQL/token" 11 | ) 12 | 13 | type DbEngine struct { 14 | Tables Tables 15 | } 16 | type Tables map[string]*Table 17 | 18 | // New Return new DbEngine struct 19 | func New() *DbEngine { 20 | engine := &DbEngine{} 21 | engine.Tables = make(Tables) 22 | 23 | return engine 24 | } 25 | 26 | // Evaluate - it takes sequences, map them to specific implementation and then process it in SQL engine 27 | func (engine *DbEngine) Evaluate(sequences *ast.Sequence) (string, error) { 28 | commands := sequences.Commands 29 | 30 | result := "" 31 | var err error 32 | for _, command := range commands { 33 | if err != nil { 34 | return "", err 35 | } 36 | switch mappedCommand := command.(type) { 37 | case *ast.WhereCommand: 38 | continue 39 | case *ast.OrderByCommand: 40 | continue 41 | case *ast.LimitCommand: 42 | continue 43 | case *ast.OffsetCommand: 44 | continue 45 | case *ast.JoinCommand: 46 | continue 47 | case *ast.CreateCommand: 48 | err = engine.createTable(mappedCommand) 49 | result += "Table '" + mappedCommand.Name.GetToken().Literal + "' has been created\n" 50 | continue 51 | case *ast.InsertCommand: 52 | err = engine.insertIntoTable(mappedCommand) 53 | result += "Data Inserted\n" 54 | continue 55 | case *ast.SelectCommand: 56 | var selectOutput *Table 57 | selectOutput, err = engine.getSelectResponse(mappedCommand) 58 | result += selectOutput.ToString() + "\n" 59 | continue 60 | case *ast.DeleteCommand: 61 | deleteCommand := command.(*ast.DeleteCommand) 62 | if deleteCommand.HasWhereCommand() { 63 | err = engine.deleteFromTable(mappedCommand, deleteCommand.WhereCommand) 64 | } 65 | result += "Data from '" + mappedCommand.Name.GetToken().Literal + "' has been deleted\n" 66 | continue 67 | case *ast.DropCommand: 68 | engine.dropTable(mappedCommand) 69 | result += "Table: '" + mappedCommand.Name.GetToken().Literal + "' has been dropped\n" 70 | continue 71 | case *ast.UpdateCommand: 72 | err = engine.updateTable(mappedCommand) 73 | result += "Table: '" + mappedCommand.Name.GetToken().Literal + "' has been updated\n" 74 | continue 75 | default: 76 | return "", &UnsupportedCommandTypeFromParserError{variable: fmt.Sprintf("%s", command)} 77 | } 78 | } 79 | return result, err 80 | } 81 | 82 | // getSelectResponse - processes a SELECT query represented by the ast.SelectCommand and applies a pipeline of 83 | // transformations based on options applied to ast.SelectCommand 84 | func (engine *DbEngine) getSelectResponse(selectCommand *ast.SelectCommand) (*Table, error) { 85 | var table *Table 86 | var err error 87 | 88 | if selectCommand.HasJoinCommand() { 89 | table, err = engine.joinTables(selectCommand.JoinCommand, selectCommand.Name.Token.Literal) 90 | } else { 91 | var exists bool 92 | table, exists = engine.Tables[selectCommand.Name.Token.Literal] 93 | if !exists { 94 | return nil, &TableDoesNotExistError{selectCommand.Name.Token.Literal} 95 | } 96 | } 97 | 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | processor := NewSelectProcessor(engine, selectCommand) 103 | 104 | // Build the transformation pipeline using the builder pattern 105 | if selectCommand.HasOrderByCommand() { 106 | processor.WithOrderByClause() 107 | } 108 | 109 | if selectCommand.HasWhereCommand() { 110 | processor.WithWhereClause() 111 | } 112 | 113 | // If no WHERE or ORDER BY, the vanilla select (projection) is applied first. 114 | // Otherwise, WHERE/ORDER BY are applied, and then the projection happens within them (handled by their respective transformers). 115 | if !selectCommand.HasOrderByCommand() && !selectCommand.HasWhereCommand() { 116 | processor.WithVanillaSelectClause() 117 | } 118 | 119 | if selectCommand.HasOffsetCommand() || selectCommand.HasLimitCommand() { 120 | processor.WithOffsetLimitClause() 121 | } 122 | 123 | if selectCommand.HasDistinct { 124 | processor.WithDistinctClause() 125 | } 126 | 127 | return processor.Process(table) 128 | } 129 | 130 | // createTable - initialize new table in engine with specified name 131 | func (engine *DbEngine) createTable(command *ast.CreateCommand) error { 132 | _, exist := engine.Tables[command.Name.Token.Literal] 133 | 134 | if exist { 135 | return &TableAlreadyExistsError{command.Name.Token.Literal} 136 | } 137 | 138 | engine.Tables[command.Name.Token.Literal] = &Table{Columns: []*Column{}} 139 | for i, columnName := range command.ColumnNames { 140 | engine.Tables[command.Name.Token.Literal].Columns = append(engine.Tables[command.Name.Token.Literal].Columns, 141 | &Column{ 142 | Type: command.ColumnTypes[i], 143 | Values: make([]ValueInterface, 0), 144 | Name: columnName, 145 | }) 146 | } 147 | return nil 148 | } 149 | 150 | func (engine *DbEngine) updateTable(command *ast.UpdateCommand) error { 151 | table, exists := engine.Tables[command.Name.Token.Literal] 152 | if !exists { 153 | return &TableDoesNotExistError{command.Name.Token.Literal} 154 | } 155 | 156 | columnIndices := make(map[string]int, len(table.Columns)) 157 | for i, col := range table.Columns { 158 | columnIndices[col.Name] = i 159 | } 160 | 161 | // Map changes to column indices 162 | type change struct { 163 | index int 164 | value ast.Anonymitifier 165 | } 166 | 167 | changes := make([]change, 0, len(command.Changes)) 168 | for colToken, newValue := range command.Changes { 169 | colName := colToken.Literal 170 | colIndex, ok := columnIndices[colName] 171 | if !ok { 172 | return &ColumnDoesNotExistError{ 173 | tableName: command.Name.Token.Literal, 174 | columnName: colName, 175 | } 176 | } 177 | changes = append(changes, change{index: colIndex, value: newValue}) 178 | } 179 | 180 | for rowIndex := 0; rowIndex < len(table.Columns[0].Values); rowIndex++ { 181 | if command.HasWhereCommand() { 182 | matches, err := isFulfillingFilters(getRow(table, rowIndex), command.WhereCommand.Expression, command.WhereCommand.Token.Literal) 183 | if err != nil { 184 | return err 185 | } 186 | if !matches { 187 | continue 188 | } 189 | } 190 | 191 | for _, change := range changes { 192 | val, err := getInterfaceValue(change.value.GetToken()) 193 | if err != nil { 194 | return err 195 | } 196 | table.Columns[change.index].Values[rowIndex] = val 197 | } 198 | } 199 | 200 | return nil 201 | } 202 | 203 | // insertIntoTable - Insert row of values into the table 204 | func (engine *DbEngine) insertIntoTable(command *ast.InsertCommand) error { 205 | table, exist := engine.Tables[command.Name.Token.Literal] 206 | if !exist { 207 | return &TableDoesNotExistError{command.Name.Token.Literal} 208 | } 209 | 210 | columns := table.Columns 211 | 212 | if len(command.Values) != len(columns) { 213 | return &InvalidNumberOfParametersError{ 214 | expectedNumber: len(columns), 215 | actualNumber: len(command.Values), 216 | commandName: command.Token.Literal, 217 | } 218 | } 219 | 220 | for i := range columns { 221 | expectedToken := tokenMapper(columns[i].Type.Type) 222 | colValueType := command.Values[i].Type 223 | 224 | if (expectedToken != colValueType) && (colValueType != token.NULL) { 225 | return &InvalidValueTypeError{ 226 | expectedType: string(expectedToken), 227 | actualType: string(colValueType), 228 | commandName: command.Token.Literal, 229 | } 230 | } 231 | interfaceValue, err := getInterfaceValue(command.Values[i]) 232 | if err != nil { 233 | return err 234 | } 235 | columns[i].Values = append(columns[i].Values, interfaceValue) 236 | } 237 | return nil 238 | } 239 | 240 | func (engine *DbEngine) selectFromProvidedTable(command *ast.SelectCommand, table *Table) (*Table, error) { 241 | columns := table.Columns 242 | 243 | wantedColumnNames := make([]string, 0) 244 | if command.AggregateFunctionAppears() { 245 | selectedTable := &Table{Columns: make([]*Column, 0)} 246 | 247 | for i := 0; i < len(command.Space); i++ { 248 | col := &Column{} 249 | var columnValues []ValueInterface 250 | currentSpace := command.Space[i] 251 | 252 | if currentSpace.ColumnName.Type == token.ASTERISK && currentSpace.AggregateFunc.Type == token.COUNT { 253 | if len(columns) > 0 { 254 | columnValues = columns[0].Values 255 | } 256 | } else { 257 | var err error 258 | columnValues, err = getValuesOfColumn(currentSpace.ColumnName.Literal, columns) 259 | if err != nil { 260 | return nil, err 261 | } 262 | } 263 | 264 | if currentSpace.ContainsAggregateFunc() { 265 | col.Name = fmt.Sprintf("%s(%s)", currentSpace.AggregateFunc.Literal, 266 | currentSpace.ColumnName.Literal) 267 | col.Type = evaluateColumnTypeOfAggregateFunc(currentSpace) 268 | aggregatedValue, err := aggregateColumnContent(currentSpace, columnValues) 269 | if err != nil { 270 | return nil, err 271 | } 272 | col.Values = []ValueInterface{aggregatedValue} 273 | } else { 274 | col.Name = currentSpace.ColumnName.Literal 275 | col.Type = currentSpace.ColumnName 276 | col.Values = []ValueInterface{columnValues[0]} 277 | } 278 | selectedTable.Columns = append(selectedTable.Columns, col) 279 | } 280 | return selectedTable, nil 281 | } else if command.Space[0].ColumnName.Type == token.ASTERISK { 282 | for i := 0; i < len(columns); i++ { 283 | wantedColumnNames = append(wantedColumnNames, columns[i].Name) 284 | } 285 | return extractColumnContent(columns, &wantedColumnNames, command.Name.GetToken().Literal) 286 | } else { 287 | for i := 0; i < len(command.Space); i++ { 288 | wantedColumnNames = append(wantedColumnNames, command.Space[i].ColumnName.Literal) 289 | } 290 | return extractColumnContent(columns, unique(wantedColumnNames), command.Name.GetToken().Literal) 291 | } 292 | } 293 | 294 | func getValuesOfColumn(columnName string, columns []*Column) ([]ValueInterface, error) { 295 | wantedColumnName := []string{columnName} 296 | columnContent, err := extractColumnContent(columns, &wantedColumnName, "") 297 | if err != nil { 298 | return nil, err 299 | } 300 | return columnContent.Columns[0].Values, nil 301 | } 302 | 303 | func evaluateColumnTypeOfAggregateFunc(space ast.Space) token.Token { 304 | if space.AggregateFunc.Type == token.MIN || 305 | space.AggregateFunc.Type == token.MAX { 306 | return space.ColumnName 307 | } 308 | return token.Token{Type: token.INT, Literal: "INT"} 309 | } 310 | 311 | func aggregateColumnContent(space ast.Space, columnValues []ValueInterface) (ValueInterface, error) { 312 | if space.AggregateFunc.Type == token.COUNT { 313 | return getCount(space, columnValues) 314 | } 315 | if len(columnValues) == 0 { 316 | return NullValue{}, nil 317 | } 318 | switch space.AggregateFunc.Type { 319 | case token.MAX: 320 | return getMax(columnValues) 321 | case token.MIN: 322 | return getMin(columnValues) 323 | case token.SUM: 324 | return getSum(columnValues) 325 | default: 326 | return getAvg(columnValues) 327 | } 328 | } 329 | 330 | func getCount(space ast.Space, columnValues []ValueInterface) (*IntegerValue, error) { 331 | if space.ColumnName.Type == token.ASTERISK { 332 | return &IntegerValue{Value: len(columnValues)}, nil 333 | } 334 | count := 0 335 | for _, value := range columnValues { 336 | if value.GetType() != NullType { 337 | count++ 338 | } 339 | } 340 | return &IntegerValue{Value: count}, nil 341 | } 342 | 343 | func getAvg(columnValues []ValueInterface) (*IntegerValue, error) { 344 | sum, err := getSum(columnValues) 345 | if err != nil { 346 | return nil, err 347 | } 348 | return &IntegerValue{Value: sum.Value / len(columnValues)}, nil 349 | } 350 | 351 | func getSum(columnValues []ValueInterface) (*IntegerValue, error) { 352 | if columnValues[0].GetType() == StringType { 353 | return &IntegerValue{Value: 0}, nil 354 | } else { 355 | sum := 0 356 | for _, value := range columnValues { 357 | if value.GetType() != NullType { 358 | num, err := strconv.Atoi(value.ToString()) 359 | if err != nil { 360 | return nil, err 361 | } 362 | sum += num 363 | } 364 | } 365 | return &IntegerValue{Value: sum}, nil 366 | } 367 | } 368 | 369 | // deleteFromTable - Delete all rows of data from table that match given condition 370 | func (engine *DbEngine) deleteFromTable(deleteCommand *ast.DeleteCommand, whereCommand *ast.WhereCommand) error { 371 | table, exist := engine.Tables[deleteCommand.Name.Token.Literal] 372 | 373 | if !exist { 374 | return &TableDoesNotExistError{deleteCommand.Name.Token.Literal} 375 | } 376 | 377 | newTable, err := engine.getFilteredTable(table, whereCommand, true, deleteCommand.Name.Token.Literal) 378 | 379 | if err != nil { 380 | return err 381 | } 382 | engine.Tables[deleteCommand.Name.Token.Literal] = newTable 383 | 384 | return nil 385 | } 386 | 387 | // dropTable - Drop table with given name 388 | func (engine *DbEngine) dropTable(dropCommand *ast.DropCommand) { 389 | delete(engine.Tables, dropCommand.Name.GetToken().Literal) 390 | } 391 | 392 | func (engine *DbEngine) getSortedTable(orderByCommand *ast.OrderByCommand, table *Table, copyOfTable *Table, tableName string) (*Table, error) { 393 | sortPatterns := orderByCommand.SortPatterns 394 | 395 | columnNames := make([]string, 0) 396 | for _, sortPattern := range sortPatterns { 397 | columnNames = append(columnNames, sortPattern.ColumnName.Literal) 398 | } 399 | 400 | missingColName := engine.getMissingColumnName(columnNames, table) 401 | if missingColName != "" { 402 | return nil, &ColumnDoesNotExistError{ 403 | tableName: tableName, 404 | columnName: missingColName, 405 | } 406 | } 407 | 408 | rows := MapTableToRows(table).rows 409 | 410 | sort.Slice(rows, func(i, j int) bool { 411 | howDeepWeSort := 0 412 | sortingType := sortPatterns[howDeepWeSort].Order.Type 413 | columnToSort := sortPatterns[howDeepWeSort].ColumnName.Literal 414 | 415 | for rows[i][columnToSort].IsEqual(rows[j][columnToSort]) { 416 | howDeepWeSort++ 417 | sortingType = sortPatterns[howDeepWeSort].Order.Type 418 | 419 | if howDeepWeSort >= len(orderByCommand.SortPatterns) { 420 | return true 421 | } 422 | columnToSort = sortPatterns[howDeepWeSort].ColumnName.Literal 423 | } 424 | 425 | if sortingType == token.DESC { 426 | return rows[i][columnToSort].isGreaterThan(rows[j][columnToSort]) 427 | } 428 | 429 | return rows[i][columnToSort].isSmallerThan(rows[j][columnToSort]) 430 | }) 431 | 432 | for _, row := range rows { 433 | for _, newColumn := range copyOfTable.Columns { 434 | value := row[newColumn.Name] 435 | newColumn.Values = append(newColumn.Values, value) 436 | } 437 | } 438 | return copyOfTable, nil 439 | } 440 | 441 | func (engine *DbEngine) getMissingColumnName(columnNames []string, table *Table) string { 442 | for _, columnName := range columnNames { 443 | exists := false 444 | for _, column := range table.Columns { 445 | if column.Name == columnName { 446 | exists = true 447 | break 448 | } 449 | } 450 | if !exists { 451 | return columnName 452 | } 453 | } 454 | return "" 455 | } 456 | 457 | func (engine *DbEngine) getFilteredTable(table *Table, whereCommand *ast.WhereCommand, negation bool, tableName string) (*Table, error) { 458 | filteredTable := getCopyOfTableWithoutRows(table) 459 | 460 | identifiers := whereCommand.Expression.GetIdentifiers() 461 | columnNames := make([]string, 0) 462 | for _, identifier := range identifiers { 463 | columnNames = append(columnNames, identifier.Token.Literal) 464 | } 465 | missingColumnName := engine.getMissingColumnName(columnNames, table) 466 | if missingColumnName != "" { 467 | return nil, &ColumnDoesNotExistError{tableName: tableName, columnName: missingColumnName} 468 | } 469 | 470 | for _, row := range MapTableToRows(table).rows { 471 | fulfilledFilters, err := isFulfillingFilters(row, whereCommand.Expression, whereCommand.Token.Literal) 472 | if err != nil { 473 | return nil, err 474 | } 475 | 476 | if xor(fulfilledFilters, negation) { 477 | for _, filteredColumn := range filteredTable.Columns { 478 | value := row[filteredColumn.Name] 479 | filteredColumn.Values = append(filteredColumn.Values, value) 480 | } 481 | } 482 | } 483 | return filteredTable, nil 484 | } 485 | 486 | func (engine *DbEngine) joinTables(joinCommand *ast.JoinCommand, leftTableName string) (*Table, error) { 487 | leftTable, exist := engine.Tables[leftTableName] 488 | leftTablePrefix := leftTableName + "." 489 | if !exist { 490 | return nil, &TableDoesNotExistError{leftTableName} 491 | } 492 | 493 | rightTableName := joinCommand.Name.Token.Literal 494 | rightTablePrefix := rightTableName + "." 495 | rightTable, exist := engine.Tables[rightTableName] 496 | if !exist { 497 | return nil, &TableDoesNotExistError{rightTableName} 498 | } 499 | 500 | joinedTable := &Table{Columns: []*Column{}} 501 | 502 | addColumnsWithPrefix(joinedTable, leftTable.Columns, leftTablePrefix) 503 | addColumnsWithPrefix(joinedTable, rightTable.Columns, rightTablePrefix) 504 | 505 | leftTableWithAddedPrefix := leftTable.getTableCopyWithAddedPrefixToColumnNames(leftTablePrefix) 506 | rightTableWithAddedPrefix := rightTable.getTableCopyWithAddedPrefixToColumnNames(rightTablePrefix) 507 | var unmatchedRightRows = make(map[int]bool) 508 | 509 | for leftRowIndex := 0; leftRowIndex < len(leftTable.Columns[0].Values); leftRowIndex++ { 510 | joinedRowLeft := getRow(leftTableWithAddedPrefix, leftRowIndex) 511 | leftRowMatches := false 512 | 513 | for rightRowIndex := 0; rightRowIndex < len(rightTable.Columns[0].Values); rightRowIndex++ { 514 | joinedRowRight := getRow(rightTableWithAddedPrefix, rightRowIndex) 515 | maps.Copy(joinedRowRight, joinedRowLeft) 516 | 517 | fulfilledFilters, err := isFulfillingFilters(joinedRowRight, joinCommand.Expression, joinCommand.Token.Literal) 518 | if err != nil { 519 | return nil, err 520 | } 521 | 522 | isLastLeftRow := leftRowIndex == len(leftTable.Columns[0].Values)-1 523 | 524 | if fulfilledFilters { 525 | for colIndex, column := range joinedTable.Columns { 526 | joinedTable.Columns[colIndex].Values = append(joinedTable.Columns[colIndex].Values, joinedRowRight[column.Name]) 527 | } 528 | leftRowMatches, unmatchedRightRows[rightRowIndex] = true, true 529 | } else if isLastLeftRow && joinCommand.ShouldTakeRightSide() && !unmatchedRightRows[rightRowIndex] { 530 | joinedRowRight = getRow(rightTableWithAddedPrefix, rightRowIndex) 531 | aggregateRowIntoJoinTable(leftTableWithAddedPrefix, joinedRowRight, joinedTable) 532 | } 533 | } 534 | 535 | if joinCommand.ShouldTakeLeftSide() && !leftRowMatches { 536 | aggregateRowIntoJoinTable(rightTableWithAddedPrefix, joinedRowLeft, joinedTable) 537 | } 538 | } 539 | 540 | return joinedTable, nil 541 | } 542 | 543 | func aggregateRowIntoJoinTable(tableWithAddedPrefix *Table, joinedRow map[string]ValueInterface, joinedTable *Table) { 544 | joinedEmptyRow := getEmptyRow(tableWithAddedPrefix) 545 | maps.Copy(joinedRow, joinedEmptyRow) 546 | for colIndex, column := range joinedTable.Columns { 547 | joinedTable.Columns[colIndex].Values = append(joinedTable.Columns[colIndex].Values, joinedRow[column.Name]) 548 | } 549 | } 550 | 551 | func addColumnsWithPrefix(finalTable *Table, columnsToAdd []*Column, prefix string) { 552 | for _, column := range columnsToAdd { 553 | finalTable.Columns = append(finalTable.Columns, 554 | &Column{ 555 | Type: column.Type, 556 | Values: make([]ValueInterface, 0), 557 | Name: prefix + column.Name, 558 | }) 559 | } 560 | } 561 | 562 | func xor(fulfilledFilters bool, negation bool) bool { 563 | return (fulfilledFilters || negation) && !(fulfilledFilters && negation) 564 | } 565 | 566 | func getCopyOfTableWithoutRows(table *Table) *Table { 567 | filteredTable := &Table{Columns: []*Column{}} 568 | 569 | for _, column := range table.Columns { 570 | filteredTable.Columns = append(filteredTable.Columns, 571 | &Column{ 572 | Type: column.Type, 573 | Values: make([]ValueInterface, 0), 574 | Name: column.Name, 575 | }) 576 | } 577 | return filteredTable 578 | } 579 | 580 | func isFulfillingFilters(row map[string]ValueInterface, expressionTree ast.Expression, commandName string) (bool, error) { 581 | switch mappedExpression := expressionTree.(type) { 582 | case *ast.OperationExpression: 583 | return processOperationExpression(row, mappedExpression, commandName) 584 | case *ast.BooleanExpression: 585 | return processBooleanExpression(mappedExpression), nil 586 | case *ast.ConditionExpression: 587 | return processConditionExpression(row, mappedExpression, commandName) 588 | case *ast.ContainExpression: 589 | return processContainExpression(row, mappedExpression) 590 | 591 | default: 592 | return false, &UnsupportedExpressionTypeError{commandName: commandName, variable: fmt.Sprintf("%s", mappedExpression)} 593 | } 594 | } 595 | 596 | func processConditionExpression(row map[string]ValueInterface, conditionExpression *ast.ConditionExpression, commandName string) (bool, error) { 597 | valueLeft, err := getTifierValue(conditionExpression.Left, row) 598 | if err != nil { 599 | return false, err 600 | } 601 | 602 | valueRight, err := getTifierValue(conditionExpression.Right, row) 603 | if err != nil { 604 | return false, err 605 | } 606 | 607 | switch conditionExpression.Condition.Type { 608 | case token.EQUAL: 609 | return valueLeft.IsEqual(valueRight), nil 610 | case token.NOT: 611 | return !(valueLeft.IsEqual(valueRight)), nil 612 | default: 613 | return false, &UnsupportedConditionalTokenError{variable: conditionExpression.Condition.Literal, commandName: commandName} 614 | } 615 | } 616 | 617 | func processContainExpression(row map[string]ValueInterface, containExpression *ast.ContainExpression) (bool, error) { 618 | valueLeft, err := getTifierValue(containExpression.Left, row) 619 | if err != nil { 620 | return false, err 621 | } 622 | 623 | result, err := ifValueInterfaceInArray(containExpression.Right, valueLeft) 624 | 625 | if containExpression.Contains { 626 | return result, err 627 | } 628 | 629 | return !result, err 630 | } 631 | 632 | func ifValueInterfaceInArray(array []ast.Anonymitifier, valueLeft ValueInterface) (bool, error) { 633 | for _, expectedValue := range array { 634 | value, err := getInterfaceValue(expectedValue.Token) 635 | if err != nil { 636 | return false, err 637 | } 638 | if value.IsEqual(valueLeft) { 639 | return true, nil 640 | } 641 | } 642 | return false, nil 643 | } 644 | 645 | func processOperationExpression(row map[string]ValueInterface, operationExpression *ast.OperationExpression, commandName string) (bool, error) { 646 | if operationExpression.Operation.Type == token.AND { 647 | left, err := isFulfillingFilters(row, operationExpression.Left, commandName) 648 | if !left { 649 | return left, err 650 | } 651 | right, err := isFulfillingFilters(row, operationExpression.Right, commandName) 652 | 653 | return right, err 654 | } 655 | 656 | if operationExpression.Operation.Type == token.OR { 657 | left, err := isFulfillingFilters(row, operationExpression.Left, commandName) 658 | if left { 659 | return left, err 660 | } 661 | right, err := isFulfillingFilters(row, operationExpression.Right, commandName) 662 | 663 | return right, err 664 | } 665 | 666 | return false, &UnsupportedOperationTokenError{operationExpression.Operation.Literal} 667 | } 668 | 669 | func processBooleanExpression(booleanExpression *ast.BooleanExpression) bool { 670 | if booleanExpression.Boolean.Literal == token.TRUE { 671 | return true 672 | } 673 | return false 674 | } 675 | 676 | func getTifierValue(tifier ast.Tifier, row map[string]ValueInterface) (ValueInterface, error) { 677 | switch mappedTifier := tifier.(type) { 678 | case ast.Identifier: 679 | value, ok := row[mappedTifier.GetToken().Literal] 680 | if ok == false { 681 | return nil, &ColumnDoesNotExistError{tableName: "", columnName: mappedTifier.GetToken().Literal} 682 | } 683 | return value, nil 684 | case ast.Anonymitifier: 685 | return getInterfaceValue(mappedTifier.GetToken()) 686 | default: 687 | return nil, &UnsupportedValueType{tifier.GetToken().Literal} 688 | } 689 | } 690 | -------------------------------------------------------------------------------- /engine/engine_error_handling_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/LissaGreense/GO4SQL/lexer" 5 | "github.com/LissaGreense/GO4SQL/parser" 6 | "github.com/LissaGreense/GO4SQL/token" 7 | "testing" 8 | ) 9 | 10 | type errorHandlingTestSuite struct { 11 | input string 12 | expectedError string 13 | } 14 | 15 | func TestEngineCreateCommandErrorHandling(t *testing.T) { 16 | duplicateTableNameError := TableAlreadyExistsError{"table1"} 17 | 18 | tests := []errorHandlingTestSuite{ 19 | {"CREATE TABLE table1( one TEXT , two INT);CREATE TABLE table1(two INT);", duplicateTableNameError.Error()}, 20 | } 21 | 22 | runEngineErrorHandlingSuite(t, tests) 23 | } 24 | 25 | func TestEngineInsertCommandErrorHandling(t *testing.T) { 26 | tableDoNotExistError := TableDoesNotExistError{"table1"} 27 | invalidNumberOfParametersError := InvalidNumberOfParametersError{expectedNumber: 2, actualNumber: 1, commandName: token.INSERT} 28 | invalidParametersTypeError := InvalidValueTypeError{expectedType: token.IDENT, actualType: token.LITERAL, commandName: token.INSERT} 29 | tests := []errorHandlingTestSuite{ 30 | {"INSERT INTO table1 VALUES( 'hello', 1);", tableDoNotExistError.Error()}, 31 | {"CREATE TABLE table1( one TEXT , two INT); INSERT INTO table1 VALUES(1);", invalidNumberOfParametersError.Error()}, 32 | {"CREATE TABLE table1( one TEXT , two INT); INSERT INTO table1 VALUES(1, 1 );", invalidParametersTypeError.Error()}, 33 | } 34 | 35 | runEngineErrorHandlingSuite(t, tests) 36 | } 37 | 38 | func TestEngineSelectCommandErrorHandling(t *testing.T) { 39 | noTableDoesNotExist := TableDoesNotExistError{"tb1"} 40 | columnDoesNotExist := ColumnDoesNotExistError{tableName: "tbl", columnName: "two"} 41 | 42 | tests := []errorHandlingTestSuite{ 43 | {"CREATE TABLE tbl(one TEXT); SELECT * FROM tb1;", noTableDoesNotExist.Error()}, 44 | {"CREATE TABLE tbl(one TEXT); SELECT two FROM tbl;", columnDoesNotExist.Error()}, 45 | } 46 | 47 | runEngineErrorHandlingSuite(t, tests) 48 | } 49 | 50 | func TestEngineDeleteCommandErrorHandling(t *testing.T) { 51 | noTableDoesNotExist := TableDoesNotExistError{"tb1"} 52 | 53 | tests := []errorHandlingTestSuite{ 54 | {"CREATE TABLE tbl(one TEXT); DELETE FROM tb1 WHERE one EQUAL 3;", noTableDoesNotExist.Error()}, 55 | } 56 | 57 | runEngineErrorHandlingSuite(t, tests) 58 | } 59 | 60 | func TestEngineWhereCommandErrorHandling(t *testing.T) { 61 | columnDoesNotExist := ColumnDoesNotExistError{tableName: "tbl", columnName: "two"} 62 | 63 | tests := []errorHandlingTestSuite{ 64 | {"CREATE TABLE tbl(one TEXT); INSERT INTO tbl VALUES('hello'); SELECT * FROM tbl WHERE two EQUAL 3;", columnDoesNotExist.Error()}, 65 | } 66 | 67 | runEngineErrorHandlingSuite(t, tests) 68 | } 69 | 70 | func TestEngineUpdateCommandErrorHandling(t *testing.T) { 71 | noTableDoesNotExist := TableDoesNotExistError{"tb1"} 72 | columnDoesNotExist := ColumnDoesNotExistError{tableName: "tbl", columnName: "two"} 73 | 74 | tests := []errorHandlingTestSuite{ 75 | {"CREATE TABLE tbl(one TEXT); UPDATE tb1 SET one TO 2;", noTableDoesNotExist.Error()}, 76 | {"CREATE TABLE tbl(one TEXT);UPDATE tbl SET two TO 2;", columnDoesNotExist.Error()}, 77 | } 78 | 79 | runEngineErrorHandlingSuite(t, tests) 80 | } 81 | 82 | func TestEngineOrderByCommandErrorHandling(t *testing.T) { 83 | columnDoesNotExist := ColumnDoesNotExistError{tableName: "tbl", columnName: "two"} 84 | 85 | tests := []errorHandlingTestSuite{ 86 | {"CREATE TABLE tbl(one TEXT); SELECT * FROM tbl ORDER BY two ASC;", columnDoesNotExist.Error()}, 87 | } 88 | 89 | runEngineErrorHandlingSuite(t, tests) 90 | } 91 | 92 | func TestEngineFullJoinErrorHandling(t *testing.T) { 93 | leftTableNotExist := TableDoesNotExistError{tableName: "leftTable"} 94 | rightTableNotExist := TableDoesNotExistError{tableName: "rightTable"} 95 | columnDoesNotExist := ColumnDoesNotExistError{tableName: "", columnName: "leftTable.two"} 96 | 97 | tests := []errorHandlingTestSuite{ 98 | {"CREATE TABLE rightTable(one TEXT); SELECT leftTable.one, rightTable.one FROM leftTable JOIN rightTable ON leftTable.one EQUAL rightTable.one;", leftTableNotExist.Error()}, 99 | {"CREATE TABLE leftTable(one TEXT); SELECT leftTable.one, rightTable.one FROM leftTable JOIN rightTable ON leftTable.one EQUAL rightTable.one;", rightTableNotExist.Error()}, 100 | {"CREATE TABLE leftTable(one TEXT); CREATE TABLE rightTable(one TEXT); INSERT INTO leftTable VALUES('hi'); INSERT INTO rightTable VALUES('hi'); SELECT * FROM leftTable JOIN rightTable ON leftTable.two EQUAL rightTable.one;", columnDoesNotExist.Error()}, 101 | } 102 | 103 | runEngineErrorHandlingSuite(t, tests) 104 | } 105 | 106 | func runEngineErrorHandlingSuite(t *testing.T, suite []errorHandlingTestSuite) { 107 | for i, test := range suite { 108 | errorMsg := getErrorMessage(t, test.input, i) 109 | 110 | if errorMsg != test.expectedError { 111 | t.Fatalf("[%v]Was expecting error: \n\t{%s},\n\tbut it was:\n\t{%s}", i, test.expectedError, errorMsg) 112 | } 113 | } 114 | } 115 | 116 | func getErrorMessage(t *testing.T, input string, testIndex int) string { 117 | lexerInstance := lexer.RunLexer(input) 118 | parserInstance := parser.New(lexerInstance) 119 | sequences, parserError := parserInstance.ParseSequence() 120 | if parserError != nil { 121 | t.Fatalf("[%d] Error has occurred in parser not in engine, error: %s", testIndex, parserError.Error()) 122 | } 123 | 124 | engine := New() 125 | _, engineError := engine.Evaluate(sequences) 126 | if engineError == nil { 127 | t.Fatalf("[%d] Was expecting error from engine but there was none", testIndex) 128 | } 129 | 130 | return engineError.Error() 131 | } 132 | -------------------------------------------------------------------------------- /engine/engine_utils.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/LissaGreense/GO4SQL/token" 7 | ) 8 | 9 | func getInterfaceValue(t token.Token) (ValueInterface, error) { 10 | switch t.Type { 11 | case token.NULL: 12 | return NullValue{}, nil 13 | case token.LITERAL: 14 | castedInteger, err := strconv.Atoi(t.Literal) 15 | if err != nil { 16 | return nil, err 17 | } 18 | return IntegerValue{Value: castedInteger}, nil 19 | default: 20 | return StringValue{Value: t.Literal}, nil 21 | } 22 | } 23 | 24 | func tokenMapper(inputToken token.Type) token.Type { 25 | switch inputToken { 26 | case token.TEXT: 27 | return token.IDENT 28 | case token.INT: 29 | return token.LITERAL 30 | default: 31 | return inputToken 32 | } 33 | } 34 | 35 | func unique(arr []string) *[]string { 36 | occurred := map[string]bool{} 37 | var result []string 38 | 39 | for e := range arr { 40 | if !occurred[arr[e]] { 41 | occurred[arr[e]] = true 42 | result = append(result, arr[e]) 43 | } 44 | } 45 | return &result 46 | } 47 | -------------------------------------------------------------------------------- /engine/errors.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import "strconv" 4 | 5 | // TableAlreadyExistsError - error thrown when a user tries to create table using name that already 6 | // exists in a database 7 | type TableAlreadyExistsError struct { 8 | tableName string 9 | } 10 | 11 | func (m *TableAlreadyExistsError) Error() string { 12 | return "table with the name of " + m.tableName + " already exists" 13 | } 14 | 15 | // TableDoesNotExistError - error thrown when the user tries to make operation on an unexisting table 16 | type TableDoesNotExistError struct { 17 | tableName string 18 | } 19 | 20 | func (m *TableDoesNotExistError) Error() string { 21 | return "table with the name of " + m.tableName + " doesn't exist" 22 | } 23 | 24 | // ColumnDoesNotExistError - error thrown when the user tries to make operation on an unexisting column 25 | type ColumnDoesNotExistError struct { 26 | tableName string 27 | columnName string 28 | } 29 | 30 | func (m *ColumnDoesNotExistError) Error() string { 31 | return "column with the name of " + m.columnName + " doesn't exist in table " + m.tableName 32 | } 33 | 34 | // InvalidNumberOfParametersError - error thrown when user provides invalid number of expected parameters 35 | // (ex. fewer values in insert than defined) 36 | type InvalidNumberOfParametersError struct { 37 | expectedNumber int 38 | actualNumber int 39 | commandName string 40 | } 41 | 42 | func (m *InvalidNumberOfParametersError) Error() string { 43 | return "invalid number of parameters in " + m.commandName + " command, should be: " + strconv.Itoa(m.expectedNumber) + ", but got: " + strconv.Itoa(m.actualNumber) 44 | } 45 | 46 | // InvalidValueTypeError - error thrown when a user provides value of a different type than expected 47 | type InvalidValueTypeError struct { 48 | expectedType string 49 | actualType string 50 | commandName string 51 | } 52 | 53 | func (m *InvalidValueTypeError) Error() string { 54 | return "invalid value type provided in " + m.commandName + " command, expecting: " + m.expectedType + ", got: " + m.actualType 55 | } 56 | 57 | // UnsupportedValueType - error thrown when the engine found unsupported data type to be stored inside 58 | // the columns 59 | type UnsupportedValueType struct { 60 | variable string 61 | } 62 | 63 | func (m *UnsupportedValueType) Error() string { 64 | return "couldn't map interface to any implementation of it: " + m.variable 65 | } 66 | 67 | // UnsupportedOperationTokenError - error thrown when engine found unsupported operation token 68 | // (supported are: AND, OR) 69 | type UnsupportedOperationTokenError struct { 70 | variable string 71 | } 72 | 73 | func (m *UnsupportedOperationTokenError) Error() string { 74 | return "unsupported operation token has been used: " + m.variable 75 | } 76 | 77 | // UnsupportedConditionalTokenError - error thrown when the engine found unsupported conditional token 78 | // inside the expression (supported are: EQUAL, NOT) 79 | type UnsupportedConditionalTokenError struct { 80 | variable string 81 | commandName string 82 | } 83 | 84 | func (m *UnsupportedConditionalTokenError) Error() string { 85 | return "operation '" + m.variable + "' provided in " + m.commandName + " command isn't allowed" 86 | } 87 | 88 | // UnsupportedExpressionTypeError - error thrown when the engine found an unsupported expression type 89 | type UnsupportedExpressionTypeError struct { 90 | variable string 91 | commandName string 92 | } 93 | 94 | func (m *UnsupportedExpressionTypeError) Error() string { 95 | return "unsupported expression has been used in " + m.commandName + "command: " + m.variable 96 | } 97 | 98 | // UnsupportedCommandTypeFromParserError - error thrown when the engine found unsupported command 99 | // from parser 100 | type UnsupportedCommandTypeFromParserError struct { 101 | variable string 102 | } 103 | 104 | func (m *UnsupportedCommandTypeFromParserError) Error() string { 105 | return "unsupported Command detected: " + m.variable 106 | } 107 | -------------------------------------------------------------------------------- /engine/generic_value.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strconv" 7 | ) 8 | 9 | // ValueInterface - Represent all supported types of data that can be inserted into Table 10 | type ValueInterface interface { 11 | ToString() string 12 | GetType() SupportedTypes 13 | IsEqual(valueInterface ValueInterface) bool 14 | isSmallerThan(valueInterface ValueInterface) bool 15 | isGreaterThan(valueInterface ValueInterface) bool 16 | } 17 | 18 | type SupportedTypes int 19 | 20 | const ( 21 | IntType = iota 22 | StringType 23 | NullType 24 | ) 25 | 26 | // IntegerValue - Implementation of ValueInterface that is containing integer values 27 | type IntegerValue struct { 28 | Value int 29 | } 30 | 31 | // StringValue - Implementation of ValueInterface that is containing string values 32 | type StringValue struct { 33 | Value string 34 | } 35 | 36 | // NullValue - Implementation of ValueInterface that is containing null 37 | type NullValue struct { 38 | } 39 | 40 | // ToString implementations 41 | func (value IntegerValue) ToString() string { return strconv.Itoa(value.Value) } 42 | func (value StringValue) ToString() string { return value.Value } 43 | func (value NullValue) ToString() string { return "NULL" } 44 | 45 | // GetType implementations 46 | func (value IntegerValue) GetType() SupportedTypes { return IntType } 47 | func (value StringValue) GetType() SupportedTypes { return StringType } 48 | func (value NullValue) GetType() SupportedTypes { return NullType } 49 | 50 | // IsEqual implementations 51 | func (value IntegerValue) IsEqual(valueInterface ValueInterface) bool { 52 | return areEqual(value, valueInterface) 53 | } 54 | func (value StringValue) IsEqual(valueInterface ValueInterface) bool { 55 | return areEqual(value, valueInterface) 56 | } 57 | func (value NullValue) IsEqual(valueInterface ValueInterface) bool { 58 | return areEqual(value, valueInterface) 59 | } 60 | 61 | // isSmallerThan implementations 62 | func (value IntegerValue) isSmallerThan(secondValue ValueInterface) bool { 63 | nullValue, isNull := secondValue.(NullValue) 64 | if isNull { 65 | return nullValue.isGreaterThan(value) 66 | } 67 | 68 | secondValueAsInteger, isInteger := secondValue.(IntegerValue) 69 | if !isInteger { 70 | log.Fatal("Can't compare Integer with other type") 71 | } 72 | 73 | return value.Value < secondValueAsInteger.Value 74 | } 75 | 76 | func (value StringValue) isSmallerThan(secondValue ValueInterface) bool { 77 | nullValue, isNull := secondValue.(NullValue) 78 | if isNull { 79 | return nullValue.isGreaterThan(value) 80 | } 81 | 82 | secondValueAsString, isString := secondValue.(StringValue) 83 | if !isString { 84 | log.Fatal("Can't compare String with other type") 85 | } 86 | 87 | return value.Value < secondValueAsString.Value 88 | } 89 | 90 | func (value NullValue) isSmallerThan(secondValue ValueInterface) bool { 91 | _, isNull := secondValue.(NullValue) 92 | 93 | if isNull { 94 | return false 95 | } 96 | 97 | return true 98 | } 99 | 100 | // isGreaterThan implementations 101 | func (value IntegerValue) isGreaterThan(secondValue ValueInterface) bool { 102 | nullValue, isNull := secondValue.(NullValue) 103 | if isNull { 104 | return nullValue.isSmallerThan(value) 105 | } 106 | 107 | secondValueAsInteger, isInteger := secondValue.(IntegerValue) 108 | if !isInteger { 109 | log.Fatal("Can't compare Integer with other type") 110 | } 111 | 112 | return value.Value > secondValueAsInteger.Value 113 | } 114 | func (value StringValue) isGreaterThan(secondValue ValueInterface) bool { 115 | nullValue, isNull := secondValue.(NullValue) 116 | if isNull { 117 | return nullValue.isSmallerThan(value) 118 | } 119 | 120 | secondValueAsString, isString := secondValue.(StringValue) 121 | if !isString { 122 | log.Fatal("Can't compare String with other type") 123 | } 124 | 125 | return value.Value > secondValueAsString.Value 126 | } 127 | 128 | func (value NullValue) isGreaterThan(_ ValueInterface) bool { 129 | return false 130 | } 131 | 132 | func areEqual(first ValueInterface, second ValueInterface) bool { 133 | return first.GetType() == second.GetType() && first.ToString() == second.ToString() 134 | } 135 | 136 | func getMin(values []ValueInterface) (ValueInterface, error) { 137 | if len(values) == 0 { 138 | return nil, errors.New("can't extract min from empty array") 139 | } 140 | minValue := values[0] 141 | 142 | for _, value := range values[1:] { 143 | if value.isSmallerThan(minValue) { 144 | minValue = value 145 | } 146 | } 147 | return minValue, nil 148 | } 149 | 150 | func getMax(values []ValueInterface) (ValueInterface, error) { 151 | if len(values) == 0 { 152 | return nil, errors.New("can't extract max from empty array") 153 | } 154 | 155 | maxValue := values[0] 156 | for _, value := range values[1:] { 157 | if value.isGreaterThan(maxValue) { 158 | maxValue = value 159 | } 160 | } 161 | 162 | return maxValue, nil 163 | } 164 | -------------------------------------------------------------------------------- /engine/generic_value_test.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsGreaterThan(t *testing.T) { 8 | oneInt := IntegerValue{ 9 | Value: 0, 10 | } 11 | twoInt := IntegerValue{ 12 | Value: 2, 13 | } 14 | oneString := StringValue{ 15 | Value: "aaa", 16 | } 17 | twoString := StringValue{ 18 | Value: "aab", 19 | } 20 | oneNull := NullValue{} 21 | twoNull := NullValue{} 22 | 23 | if oneInt.isGreaterThan(twoInt) { 24 | t.Errorf("0 shouldn't be greater than 2") 25 | } 26 | 27 | if !twoInt.isGreaterThan(oneInt) { 28 | t.Errorf("0 shouldn't be greater than 2") 29 | } 30 | 31 | if oneString.isGreaterThan(twoString) { 32 | t.Errorf("aaa shouldn't be greater than aab") 33 | } 34 | 35 | if !twoString.isGreaterThan(oneString) { 36 | t.Errorf("1 shouldn't be greater than 2") 37 | } 38 | 39 | if twoNull.isGreaterThan(oneNull) { 40 | t.Errorf("null is not greater than null") 41 | } 42 | 43 | if !oneInt.isGreaterThan(oneNull) { 44 | t.Errorf("Any Int value cannot be smaller than null") 45 | } 46 | 47 | if !oneString.isGreaterThan(oneNull) { 48 | t.Errorf("Any String value cannot be smaller than null") 49 | } 50 | 51 | if oneNull.isGreaterThan(oneInt) { 52 | t.Errorf("Null cannot be greater than any int value") 53 | } 54 | 55 | if oneNull.isGreaterThan(oneString) { 56 | t.Errorf("Null cannot be greater than any string value") 57 | } 58 | } 59 | 60 | func TestIsSmallerThan(t *testing.T) { 61 | oneInt := IntegerValue{ 62 | Value: 0, 63 | } 64 | twoInt := IntegerValue{ 65 | Value: 2, 66 | } 67 | oneString := StringValue{ 68 | Value: "aaa", 69 | } 70 | twoString := StringValue{ 71 | Value: "aab", 72 | } 73 | oneNull := NullValue{} 74 | twoNull := NullValue{} 75 | 76 | if !oneInt.isSmallerThan(twoInt) { 77 | t.Errorf("0 should be smaller than 2") 78 | } 79 | 80 | if twoInt.isSmallerThan(oneInt) { 81 | t.Errorf("0 should be smaller than 2") 82 | } 83 | 84 | if !oneString.isSmallerThan(twoString) { 85 | t.Errorf("aaa should be smaller than aab") 86 | } 87 | 88 | if twoString.isSmallerThan(oneString) { 89 | t.Errorf("1 should be smaller than 2") 90 | } 91 | 92 | if twoNull.isSmallerThan(oneNull) { 93 | t.Errorf("null is not smaller than null") 94 | } 95 | 96 | if oneInt.isSmallerThan(oneNull) { 97 | t.Errorf("Any int value cannot be smaller than null") 98 | } 99 | 100 | if oneString.isSmallerThan(oneNull) { 101 | t.Errorf("Any string value cannot be smaller than null") 102 | } 103 | 104 | if !oneNull.isSmallerThan(oneInt) { 105 | t.Errorf("Null cannot be greater than any int value") 106 | } 107 | 108 | if !oneNull.isSmallerThan(oneString) { 109 | t.Errorf("Null cannot be greater than any string value") 110 | } 111 | } 112 | 113 | func TestEquals(t *testing.T) { 114 | oneInt := IntegerValue{ 115 | Value: 1, 116 | } 117 | twoInt := IntegerValue{ 118 | Value: 2, 119 | } 120 | oneString := StringValue{ 121 | Value: "one", 122 | } 123 | twoString := StringValue{ 124 | Value: "two", 125 | } 126 | oneNull := NullValue{} 127 | twoNull := NullValue{} 128 | 129 | shouldBeEqual(t, oneInt, oneInt) 130 | shouldBeEqual(t, oneString, oneString) 131 | shouldBeEqual(t, oneNull, twoNull) 132 | shouldNotBeEqual(t, oneInt, twoInt) 133 | shouldNotBeEqual(t, oneString, twoString) 134 | shouldNotBeEqual(t, oneString, oneInt) 135 | shouldNotBeEqual(t, oneNull, oneInt) 136 | shouldNotBeEqual(t, oneNull, oneString) 137 | shouldNotBeEqual(t, twoInt, twoNull) 138 | } 139 | 140 | func shouldBeEqual(t *testing.T, valueOne ValueInterface, valueTwo ValueInterface) { 141 | const ErrorMsgShouldBeEqual = "%s(type: %v) is not equal %s(type: %v), but is should be" 142 | 143 | if !valueOne.IsEqual(valueTwo) { 144 | t.Errorf(ErrorMsgShouldBeEqual, valueOne.ToString(), valueOne.GetType(), valueTwo.ToString(), valueTwo.GetType()) 145 | } 146 | } 147 | func shouldNotBeEqual(t *testing.T, valueOne ValueInterface, valueTwo ValueInterface) { 148 | const ErrorMsgShouldNotBeEqual = "%s(type: %v) is equal %s(type: %v), but is shouldn't be" 149 | 150 | if valueOne.IsEqual(valueTwo) { 151 | t.Errorf(ErrorMsgShouldNotBeEqual, valueOne.ToString(), valueOne.GetType(), valueTwo.ToString(), valueTwo.GetType()) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /engine/query_processor.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "hash/adler32" 5 | 6 | "github.com/LissaGreense/GO4SQL/ast" 7 | "github.com/LissaGreense/GO4SQL/token" 8 | ) 9 | 10 | // TableTransformer defines a function that takes a Table as input and then applies a transformation 11 | type TableTransformer func(*Table) (*Table, error) 12 | 13 | // SelectProcessor handles the step-by-step processing of a SELECT query using a builder pattern. 14 | type SelectProcessor struct { 15 | engine *DbEngine 16 | cmd *ast.SelectCommand 17 | transformers []TableTransformer 18 | } 19 | 20 | // NewSelectProcessor creates a new SelectProcessor. 21 | func NewSelectProcessor(engine *DbEngine, cmd *ast.SelectCommand) *SelectProcessor { 22 | return &SelectProcessor{ 23 | engine: engine, 24 | cmd: cmd, 25 | transformers: []TableTransformer{}, // Initialize as an empty slice 26 | } 27 | } 28 | 29 | // WithVanillaSelectClause adds the vanilla select (projection) transformation. 30 | func (sp *SelectProcessor) WithVanillaSelectClause() *SelectProcessor { 31 | sp.transformers = append(sp.transformers, sp.getVanillaSelectTransformer()) 32 | return sp 33 | } 34 | 35 | // WithWhereClause adds the WHERE clause transformation. 36 | func (sp *SelectProcessor) WithWhereClause() *SelectProcessor { 37 | sp.transformers = append(sp.transformers, sp.getWhereTransformer()) 38 | return sp 39 | } 40 | 41 | // WithOrderByClause adds the ORDER BY clause transformation. 42 | func (sp *SelectProcessor) WithOrderByClause() *SelectProcessor { 43 | sp.transformers = append(sp.transformers, sp.getOrderByTransformer()) 44 | return sp 45 | } 46 | 47 | // WithOffsetLimitClause adds the OFFSET and LIMIT clause transformation. 48 | func (sp *SelectProcessor) WithOffsetLimitClause() *SelectProcessor { 49 | sp.transformers = append(sp.transformers, sp.getOffsetLimitTransformer()) 50 | return sp 51 | } 52 | 53 | // WithDistinctClause adds the DISTINCT clause transformation. 54 | func (sp *SelectProcessor) WithDistinctClause() *SelectProcessor { 55 | sp.transformers = append(sp.transformers, sp.getDistinctTransformer()) 56 | return sp 57 | } 58 | 59 | // Process applies the configured pipeline of transformations to the initialTable. 60 | func (sp *SelectProcessor) Process(initialTable *Table) (*Table, error) { 61 | table := initialTable 62 | var err error 63 | 64 | for _, transform := range sp.transformers { 65 | table, err = transform(table) 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | return table, nil 71 | } 72 | 73 | // --- Private Transformer Getters --- 74 | 75 | func (sp *SelectProcessor) getVanillaSelectTransformer() TableTransformer { 76 | return func(tbl *Table) (*Table, error) { 77 | return sp.engine.selectFromProvidedTable(sp.cmd, tbl) 78 | } 79 | } 80 | 81 | func (sp *SelectProcessor) getWhereTransformer() TableTransformer { 82 | return func(tbl *Table) (*Table, error) { 83 | if len(tbl.Columns) == 0 || (len(tbl.Columns) > 0 && len(tbl.Columns[0].Values) == 0) { 84 | return sp.engine.selectFromProvidedTable(sp.cmd, &Table{Columns: []*Column{}}) 85 | } 86 | filtered, err := sp.engine.getFilteredTable(tbl, sp.cmd.WhereCommand, false, sp.cmd.Name.GetToken().Literal) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return sp.engine.selectFromProvidedTable(sp.cmd, filtered) 91 | } 92 | } 93 | 94 | func (sp *SelectProcessor) getOrderByTransformer() TableTransformer { 95 | return func(tbl *Table) (*Table, error) { 96 | emptyTable := getCopyOfTableWithoutRows(tbl) 97 | sorted, err := sp.engine.getSortedTable(sp.cmd.OrderByCommand, tbl, emptyTable, sp.cmd.Name.GetToken().Literal) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return sp.engine.selectFromProvidedTable(sp.cmd, sorted) 102 | } 103 | } 104 | 105 | func (sp *SelectProcessor) getOffsetLimitTransformer() TableTransformer { 106 | return func(tbl *Table) (*Table, error) { 107 | var offset = 0 108 | var limitRaw = -1 109 | 110 | if sp.cmd.HasLimitCommand() { 111 | limitRaw = sp.cmd.LimitCommand.Count 112 | } 113 | if sp.cmd.HasOffsetCommand() { 114 | offset = sp.cmd.OffsetCommand.Count 115 | } 116 | 117 | if len(tbl.Columns) == 0 { 118 | return tbl, nil 119 | } 120 | 121 | for _, column := range tbl.Columns { 122 | var limit int 123 | 124 | if limitRaw == -1 || limitRaw+offset > len(column.Values) { 125 | limit = len(column.Values) 126 | } else { 127 | limit = limitRaw + offset 128 | } 129 | 130 | if offset >= len(column.Values) { 131 | column.Values = make([]ValueInterface, 0) 132 | } else if offset < len(column.Values) && limit > offset { 133 | column.Values = column.Values[offset:limit] 134 | } else { 135 | column.Values = make([]ValueInterface, 0) 136 | } 137 | } 138 | return tbl, nil 139 | } 140 | } 141 | 142 | func (sp *SelectProcessor) getDistinctTransformer() TableTransformer { 143 | return func(tbl *Table) (*Table, error) { 144 | if len(tbl.Columns) == 0 || len(tbl.Columns[0].Values) == 0 { 145 | return tbl, nil 146 | } 147 | 148 | distinctTable := getCopyOfTableWithoutRows(tbl) 149 | rowsCount := len(tbl.Columns[0].Values) 150 | checksumSet := make(map[uint32]struct{}) 151 | 152 | for iRow := range rowsCount { 153 | mergedColumnValues := "" 154 | for iColumn := range tbl.Columns { 155 | if iRow < len(tbl.Columns[iColumn].Values) { 156 | fieldValue := tbl.Columns[iColumn].Values[iRow].ToString() 157 | if tbl.Columns[iColumn].Type.Literal == token.TEXT { 158 | fieldValue = "'" + fieldValue + "'" 159 | } 160 | mergedColumnValues += fieldValue 161 | } else { 162 | mergedColumnValues += "" 163 | } 164 | } 165 | checksum := adler32.Checksum([]byte(mergedColumnValues)) 166 | 167 | if _, exist := checksumSet[checksum]; !exist { 168 | checksumSet[checksum] = struct{}{} 169 | for i, column := range distinctTable.Columns { 170 | if iRow < len(tbl.Columns[i].Values) { 171 | column.Values = append(column.Values, tbl.Columns[i].Values[iRow]) 172 | } 173 | } 174 | } 175 | } 176 | return distinctTable, nil 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /engine/row.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | // Rows - Contain rows that store values, alternative to Table, some operations are easier 4 | type Rows struct { 5 | rows []map[string]ValueInterface 6 | } 7 | 8 | // MapTableToRows - transform Table struct into Rows 9 | func MapTableToRows(table *Table) Rows { 10 | rows := make([]map[string]ValueInterface, 0) 11 | 12 | numberOfRows := len(table.Columns[0].Values) 13 | 14 | for rowIndex := 0; rowIndex < numberOfRows; rowIndex++ { 15 | rows = append(rows, getRow(table, rowIndex)) 16 | } 17 | return Rows{rows: rows} 18 | } 19 | 20 | func getRow(table *Table, rowIndex int) map[string]ValueInterface { 21 | row := make(map[string]ValueInterface) 22 | for _, column := range table.Columns { 23 | row[column.Name] = column.Values[rowIndex] 24 | } 25 | return row 26 | } 27 | 28 | func getEmptyRow(table *Table) map[string]ValueInterface { 29 | row := make(map[string]ValueInterface) 30 | for _, column := range table.Columns { 31 | row[column.Name] = NullValue{} 32 | } 33 | return row 34 | } 35 | -------------------------------------------------------------------------------- /engine/table.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "github.com/LissaGreense/GO4SQL/token" 5 | ) 6 | 7 | // Table - Contain Columns that store values in engine 8 | type Table struct { 9 | Columns Columns 10 | } 11 | 12 | type Columns []*Column 13 | 14 | func (table *Table) isEqual(secondTable *Table) bool { 15 | if len(table.Columns) != len(secondTable.Columns) { 16 | return false 17 | } 18 | 19 | for i := range table.Columns { 20 | if table.Columns[i].Name != secondTable.Columns[i].Name { 21 | return false 22 | } 23 | if table.Columns[i].Type.Literal != secondTable.Columns[i].Type.Literal { 24 | return false 25 | } 26 | if table.Columns[i].Type.Type != secondTable.Columns[i].Type.Type { 27 | return false 28 | } 29 | if len(table.Columns[i].Values) != len(secondTable.Columns[i].Values) { 30 | return false 31 | } 32 | for j := range table.Columns[i].Values { 33 | if table.Columns[i].Values[j].ToString() != secondTable.Columns[i].Values[j].ToString() { 34 | return false 35 | } 36 | } 37 | } 38 | 39 | return true 40 | } 41 | 42 | // ToString - Return string contains all values and Column names in Table 43 | func (table *Table) ToString() string { 44 | if table == nil { 45 | return "" 46 | } 47 | columWidths := getColumWidths(table.Columns) 48 | bar := getBar(columWidths) 49 | result := bar + "\n" 50 | 51 | result += "|" 52 | for i := range table.Columns { 53 | result += " " 54 | for j := 0; j < columWidths[i]-len(table.Columns[i].Name); j++ { 55 | result += " " 56 | } 57 | result += table.Columns[i].Name 58 | result += " |" 59 | } 60 | result += "\n" + bar + "\n" 61 | 62 | if len(table.Columns) == 0 { 63 | return result 64 | } 65 | 66 | rowsCount := len(table.Columns[0].Values) 67 | 68 | for iRow := 0; iRow < rowsCount; iRow++ { 69 | result += "|" 70 | 71 | for iColumn := range table.Columns { 72 | result += " " 73 | 74 | printedValue := table.Columns[iColumn].Values[iRow].ToString() 75 | if table.Columns[iColumn].Type.Literal == token.TEXT && 76 | table.Columns[iColumn].Values[iRow].GetType() != NullType { 77 | printedValue = "'" + printedValue + "'" 78 | } 79 | for i := 0; i < columWidths[iColumn]-len(printedValue); i++ { 80 | result += " " 81 | } 82 | 83 | result += printedValue + " |" 84 | } 85 | 86 | result += "\n" 87 | } 88 | 89 | return result + bar 90 | } 91 | 92 | func (table *Table) getTableCopyWithAddedPrefixToColumnNames(columnNamePrefix string) *Table { 93 | newTable := &Table{Columns: []*Column{}} 94 | 95 | for _, column := range table.Columns { 96 | newTable.Columns = append(newTable.Columns, 97 | &Column{ 98 | Type: column.Type, 99 | Values: column.Values, 100 | Name: columnNamePrefix + column.Name, 101 | }) 102 | } 103 | 104 | return newTable 105 | } 106 | 107 | func getBar(columWidths []int) string { 108 | bar := "+" 109 | 110 | for i := 0; i < len(columWidths); i++ { 111 | bar += "-" 112 | for j := 0; j < columWidths[i]; j++ { 113 | bar += "-" 114 | } 115 | bar += "-+" 116 | } 117 | 118 | return bar 119 | } 120 | 121 | func getColumWidths(columns []*Column) []int { 122 | widths := make([]int, 0) 123 | 124 | for iColumn := range columns { 125 | maxLength := len(columns[iColumn].Name) 126 | for iRow := range columns[iColumn].Values { 127 | valueLength := len(columns[iColumn].Values[iRow].ToString()) 128 | if columns[iColumn].Type.Literal == token.TEXT { 129 | valueLength += 2 // double ' 130 | } 131 | if valueLength > maxLength { 132 | maxLength = valueLength 133 | } 134 | } 135 | widths = append(widths, maxLength) 136 | } 137 | 138 | return widths 139 | } 140 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/LissaGreense/GO4SQL 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /helm/GO4SQL/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /helm/GO4SQL/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: go4sql 3 | description: A Helm chart for Kubernetes 4 | type: application 5 | version: 0.0.1 6 | appVersion: "0.0.2" 7 | -------------------------------------------------------------------------------- /helm/GO4SQL/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "go4sql.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "go4sql.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "go4sql.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "go4sql.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /helm/GO4SQL/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "go4sql.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "go4sql.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "go4sql.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "go4sql.labels" -}} 37 | helm.sh/chart: {{ include "go4sql.chart" . }} 38 | {{ include "go4sql.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "go4sql.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "go4sql.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /helm/GO4SQL/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "go4sql.fullname" . }} 5 | labels: 6 | {{- include "go4sql.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | matchLabels: 10 | {{- include "go4sql.selectorLabels" . | nindent 6 }} 11 | template: 12 | metadata: 13 | {{- with .Values.podAnnotations }} 14 | annotations: 15 | {{- toYaml . | nindent 8 }} 16 | {{- end }} 17 | labels: 18 | {{- include "go4sql.labels" . | nindent 8 }} 19 | {{- with .Values.podLabels }} 20 | {{- toYaml . | nindent 8 }} 21 | {{- end }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | securityContext: 28 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 29 | containers: 30 | - name: {{ .Chart.Name }} 31 | args: 32 | - "-socket" 33 | securityContext: 34 | {{- toYaml .Values.securityContext | nindent 12 }} 35 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 36 | imagePullPolicy: {{ .Values.image.pullPolicy }} 37 | ports: 38 | - name: http 39 | containerPort: {{ .Values.service.port }} 40 | protocol: TCP 41 | livenessProbe: 42 | {{- toYaml .Values.livenessProbe | nindent 12 }} 43 | readinessProbe: 44 | {{- toYaml .Values.readinessProbe | nindent 12 }} 45 | resources: 46 | {{- toYaml .Values.resources | nindent 12 }} 47 | {{- with .Values.volumeMounts }} 48 | volumeMounts: 49 | {{- toYaml . | nindent 12 }} 50 | {{- end }} 51 | {{- with .Values.volumes }} 52 | volumes: 53 | {{- toYaml . | nindent 8 }} 54 | {{- end }} 55 | {{- with .Values.nodeSelector }} 56 | nodeSelector: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | {{- with .Values.affinity }} 60 | affinity: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.tolerations }} 64 | tolerations: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | -------------------------------------------------------------------------------- /helm/GO4SQL/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "go4sql.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "go4sql.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /helm/GO4SQL/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "go4sql.fullname" . }} 5 | labels: 6 | {{- include "go4sql.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "go4sql.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /helm/GO4SQL/values.yaml: -------------------------------------------------------------------------------- 1 | replicaCount: 1 2 | image: 3 | repository: kajedot/go4sql 4 | pullPolicy: Always 5 | tag: "latest" 6 | 7 | imagePullSecrets: [] 8 | nameOverride: "" 9 | fullnameOverride: "" 10 | podAnnotations: {} 11 | podLabels: {} 12 | 13 | podSecurityContext: {} 14 | # fsGroup: 2000 15 | 16 | securityContext: {} 17 | # capabilities: 18 | # drop: 19 | # - ALL 20 | # readOnlyRootFilesystem: true 21 | # runAsNonRoot: true 22 | # runAsUser: 1000 23 | 24 | service: 25 | type: ClusterIP 26 | port: 80 27 | 28 | ingress: 29 | enabled: false 30 | className: "" 31 | annotations: {} 32 | # kubernetes.io/ingress.class: nginx 33 | # kubernetes.io/tls-acme: "true" 34 | hosts: 35 | - host: chart-example.local 36 | paths: 37 | - path: / 38 | pathType: ImplementationSpecific 39 | tls: [] 40 | # - secretName: chart-example-tls 41 | # hosts: 42 | # - chart-example.local 43 | 44 | resources: 45 | limits: 46 | cpu: 100m 47 | memory: 528Mi 48 | requests: 49 | cpu: 100m 50 | memory: 528Mi 51 | 52 | livenessProbe: 53 | exec: 54 | command: 55 | - /bin/sh 56 | - '-c' 57 | - ls /app/go4sql-docker 58 | readinessProbe: 59 | exec: 60 | command: 61 | - /bin/sh 62 | - '-c' 63 | - ps -A | grep go4sql-docker | grep -v grep 64 | 65 | volumes: [] 66 | 67 | volumeMounts: [] 68 | 69 | nodeSelector: {} 70 | 71 | tolerations: [] 72 | 73 | affinity: {} 74 | -------------------------------------------------------------------------------- /lexer/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package lexer is converting a sequence of characters (stdin or input file) 3 | into a sequence of lexical tokens, which are later interpreted by parser. 4 | */ 5 | package lexer 6 | -------------------------------------------------------------------------------- /lexer/lexer.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/LissaGreense/GO4SQL/token" 7 | ) 8 | 9 | // Lexer - Represent the text input (commands) provided by client before tokenization 10 | type Lexer struct { 11 | input string 12 | position int // current position in input (points to current char) 13 | readPosition int // current reading position in input (after current char) 14 | character byte // current char under examination 15 | insideApostrophes bool // flag which tells if lexer is between apostrophes 16 | } 17 | 18 | // RunLexer - Start lexer by moving to the first position 19 | func RunLexer(input string) *Lexer { 20 | l := &Lexer{input: input, insideApostrophes: false} 21 | l.readChar() 22 | return l 23 | } 24 | 25 | // NextToken - Return the next token structure which is appearing after current position. 26 | func (lexer *Lexer) NextToken() token.Token { 27 | var tok token.Token 28 | 29 | lexer.skipWhitespace() 30 | 31 | switch lexer.character { 32 | case '*': 33 | tok = newToken(token.ASTERISK, string(lexer.character)) 34 | case ';': 35 | tok = newToken(token.SEMICOLON, string(lexer.character)) 36 | case ',': 37 | tok = newToken(token.COMMA, string(lexer.character)) 38 | case '(': 39 | tok = newToken(token.LPAREN, string(lexer.character)) 40 | case ')': 41 | tok = newToken(token.RPAREN, string(lexer.character)) 42 | case '\'': 43 | lexer.insideApostrophes = !lexer.insideApostrophes 44 | tok = newToken(token.APOSTROPHE, string(lexer.character)) 45 | case 0: 46 | tok.Literal = "" 47 | tok.Type = token.EOF 48 | default: 49 | if lexer.insideApostrophes { 50 | characters := lexer.processCharacters([]byte{'\''}, []byte{' ', '\n', '\t', '\r'}) 51 | return characters 52 | } else { 53 | characters := lexer.processCharacters([]byte{'\'', '(', ',', ';', '*', ')'}, []byte{}) 54 | return characters 55 | } 56 | } 57 | 58 | lexer.readChar() 59 | return tok 60 | } 61 | 62 | func (lexer *Lexer) skipWhitespace() { 63 | for isWhitespace(lexer.character) && !lexer.insideApostrophes { 64 | lexer.readChar() 65 | } 66 | } 67 | 68 | func (lexer *Lexer) readChar() { 69 | lexer.character = lexer.getNextChar() 70 | lexer.position = lexer.readPosition 71 | lexer.readPosition += 1 72 | } 73 | 74 | func (lexer *Lexer) getNextChar() byte { 75 | if lexer.readPosition >= len(lexer.input) { 76 | return 0 77 | } 78 | return lexer.input[lexer.readPosition] 79 | } 80 | 81 | func (lexer *Lexer) processCharacters(blacklist []byte, whitelist []byte) token.Token { 82 | nextChar := lexer.getNextChar() 83 | position := lexer.position 84 | 85 | hasDigit := isDigit(lexer.character) 86 | hasLetter := isLetter(lexer.character) 87 | 88 | for validChar(nextChar, whitelist) && !bytes.ContainsAny(blacklist, string(nextChar)) { 89 | lexer.readChar() 90 | nextChar = lexer.getNextChar() 91 | 92 | hasDigit = hasDigit || isDigit(lexer.character) 93 | hasLetter = hasLetter || isLetter(lexer.character) 94 | } 95 | lexer.readChar() 96 | 97 | return lexer.evaluateToken(position, hasLetter, hasDigit) 98 | } 99 | 100 | func (lexer *Lexer) evaluateToken(position int, hasLetter bool, hasDigit bool) token.Token { 101 | characters := lexer.input[position:lexer.position] 102 | 103 | if (hasLetter && hasDigit) || lexer.insideApostrophes { 104 | return newToken(token.IDENT, characters) 105 | } 106 | if hasLetter && !hasDigit { 107 | return newToken(token.LookupIdent(characters), characters) 108 | } 109 | if !hasLetter && hasDigit { 110 | return newToken(token.LITERAL, characters) 111 | } 112 | 113 | return newToken(token.ILLEGAL, characters) 114 | } 115 | 116 | func validChar(nextChar byte, whitelist []byte) bool { 117 | return '!' <= nextChar && nextChar <= '~' || bytes.ContainsAny(whitelist, string(nextChar)) 118 | } 119 | 120 | func isLetter(ch byte) bool { 121 | return 'a' <= ch && ch <= 'z' || 122 | 'A' <= ch && ch <= 'Z' 123 | } 124 | 125 | func isDigit(ch byte) bool { 126 | return '0' <= ch && ch <= '9' 127 | } 128 | 129 | func isWhitespace(ch byte) bool { 130 | return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' 131 | } 132 | 133 | func newToken(tokenType token.Type, characters string) token.Token { 134 | return token.Token{ 135 | Type: tokenType, 136 | Literal: characters, 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lexer/lexer_test.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/LissaGreense/GO4SQL/token" 7 | ) 8 | 9 | func TestLexerWithInsertCommand(t *testing.T) { 10 | input := 11 | ` 12 | CREATE TABLE 1tbl( one TEXT , two INT ); 13 | INSERT INTO tbl VALUES( 'CREATE', 10 ); 14 | INSERT INTO tbl VALUES( 'goodbye', 20 ); 15 | ` 16 | tests := []struct { 17 | expectedType token.Type 18 | expectedLiteral string 19 | }{ 20 | {token.CREATE, "CREATE"}, 21 | {token.TABLE, "TABLE"}, 22 | {token.IDENT, "1tbl"}, 23 | {token.LPAREN, "("}, 24 | {token.IDENT, "one"}, 25 | {token.TEXT, "TEXT"}, 26 | {token.COMMA, ","}, 27 | {token.IDENT, "two"}, 28 | {token.INT, "INT"}, 29 | {token.RPAREN, ")"}, 30 | {token.SEMICOLON, ";"}, 31 | {token.INSERT, "INSERT"}, 32 | {token.INTO, "INTO"}, 33 | {token.IDENT, "tbl"}, 34 | {token.VALUES, "VALUES"}, 35 | {token.LPAREN, "("}, 36 | {token.APOSTROPHE, "'"}, 37 | {token.IDENT, "CREATE"}, 38 | {token.APOSTROPHE, "'"}, 39 | {token.COMMA, ","}, 40 | {token.LITERAL, "10"}, 41 | {token.RPAREN, ")"}, 42 | {token.SEMICOLON, ";"}, 43 | {token.INSERT, "INSERT"}, 44 | {token.INTO, "INTO"}, 45 | {token.IDENT, "tbl"}, 46 | {token.VALUES, "VALUES"}, 47 | {token.LPAREN, "("}, 48 | {token.APOSTROPHE, "'"}, 49 | {token.IDENT, "goodbye"}, 50 | {token.APOSTROPHE, "'"}, 51 | {token.COMMA, ","}, 52 | {token.LITERAL, "20"}, 53 | {token.RPAREN, ")"}, 54 | {token.SEMICOLON, ";"}, 55 | {token.EOF, ""}, 56 | } 57 | 58 | runLexerTestSuite(t, input, tests) 59 | } 60 | 61 | func TestLexerWithUpdateCommand(t *testing.T) { 62 | input := 63 | ` 64 | UPDATE table1 65 | SET column_name_1 TO 'UPDATE', column_name_2 TO 42 66 | WHERE column_name_3 EQUAL 1; 67 | ` 68 | tests := []struct { 69 | expectedType token.Type 70 | expectedLiteral string 71 | }{ 72 | {token.UPDATE, "UPDATE"}, 73 | {token.IDENT, "table1"}, 74 | {token.SET, "SET"}, 75 | {token.IDENT, "column_name_1"}, 76 | {token.TO, "TO"}, 77 | {token.APOSTROPHE, "'"}, 78 | {token.IDENT, "UPDATE"}, 79 | {token.APOSTROPHE, "'"}, 80 | {token.COMMA, ","}, 81 | {token.IDENT, "column_name_2"}, 82 | {token.TO, "TO"}, 83 | {token.LITERAL, "42"}, 84 | {token.WHERE, "WHERE"}, 85 | {token.IDENT, "column_name_3"}, 86 | {token.EQUAL, "EQUAL"}, 87 | {token.LITERAL, "1"}, 88 | {token.SEMICOLON, ";"}, 89 | {token.EOF, ""}, 90 | } 91 | 92 | runLexerTestSuite(t, input, tests) 93 | } 94 | 95 | func TestLexerWithNumbersMixedInLiterals(t *testing.T) { 96 | input := 97 | ` 98 | CREATE TABLE tbl2( one TEXT , two INT ); 99 | INSERT INTO tbl2 VALUES( 'hello1', 10 ); 100 | INSERT INTO tbl2 VALUES( 'good123bye', 20 ); 101 | ` 102 | tests := []struct { 103 | expectedType token.Type 104 | expectedLiteral string 105 | }{ 106 | {token.CREATE, "CREATE"}, 107 | {token.TABLE, "TABLE"}, 108 | {token.IDENT, "tbl2"}, 109 | {token.LPAREN, "("}, 110 | {token.IDENT, "one"}, 111 | {token.TEXT, "TEXT"}, 112 | {token.COMMA, ","}, 113 | {token.IDENT, "two"}, 114 | {token.INT, "INT"}, 115 | {token.RPAREN, ")"}, 116 | {token.SEMICOLON, ";"}, 117 | {token.INSERT, "INSERT"}, 118 | {token.INTO, "INTO"}, 119 | {token.IDENT, "tbl2"}, 120 | {token.VALUES, "VALUES"}, 121 | {token.LPAREN, "("}, 122 | {token.APOSTROPHE, "'"}, 123 | {token.IDENT, "hello1"}, 124 | {token.APOSTROPHE, "'"}, 125 | {token.COMMA, ","}, 126 | {token.LITERAL, "10"}, 127 | {token.RPAREN, ")"}, 128 | {token.SEMICOLON, ";"}, 129 | {token.INSERT, "INSERT"}, 130 | {token.INTO, "INTO"}, 131 | {token.IDENT, "tbl2"}, 132 | {token.VALUES, "VALUES"}, 133 | {token.LPAREN, "("}, 134 | {token.APOSTROPHE, "'"}, 135 | {token.IDENT, "good123bye"}, 136 | {token.APOSTROPHE, "'"}, 137 | {token.COMMA, ","}, 138 | {token.LITERAL, "20"}, 139 | {token.RPAREN, ")"}, 140 | {token.SEMICOLON, ";"}, 141 | {token.EOF, ""}, 142 | } 143 | 144 | runLexerTestSuite(t, input, tests) 145 | } 146 | 147 | func TestLexerWithNumbersWithWhitespacesIdentifier(t *testing.T) { 148 | input := 149 | ` 150 | CREATE TABLE tbl2( one TEXT , two INT ); 151 | INSERT INTO tbl2 VALUES( ' hello1', 10 ); 152 | INSERT INTO tbl2 VALUES( 'Hello Dear', 20 ); 153 | ` 154 | tests := []struct { 155 | expectedType token.Type 156 | expectedLiteral string 157 | }{ 158 | {token.CREATE, "CREATE"}, 159 | {token.TABLE, "TABLE"}, 160 | {token.IDENT, "tbl2"}, 161 | {token.LPAREN, "("}, 162 | {token.IDENT, "one"}, 163 | {token.TEXT, "TEXT"}, 164 | {token.COMMA, ","}, 165 | {token.IDENT, "two"}, 166 | {token.INT, "INT"}, 167 | {token.RPAREN, ")"}, 168 | {token.SEMICOLON, ";"}, 169 | {token.INSERT, "INSERT"}, 170 | {token.INTO, "INTO"}, 171 | {token.IDENT, "tbl2"}, 172 | {token.VALUES, "VALUES"}, 173 | {token.LPAREN, "("}, 174 | {token.APOSTROPHE, "'"}, 175 | {token.IDENT, " hello1"}, 176 | {token.APOSTROPHE, "'"}, 177 | {token.COMMA, ","}, 178 | {token.LITERAL, "10"}, 179 | {token.RPAREN, ")"}, 180 | {token.SEMICOLON, ";"}, 181 | {token.INSERT, "INSERT"}, 182 | {token.INTO, "INTO"}, 183 | {token.IDENT, "tbl2"}, 184 | {token.VALUES, "VALUES"}, 185 | {token.LPAREN, "("}, 186 | {token.APOSTROPHE, "'"}, 187 | {token.IDENT, "Hello\t Dear"}, 188 | {token.APOSTROPHE, "'"}, 189 | {token.COMMA, ","}, 190 | {token.LITERAL, "20"}, 191 | {token.RPAREN, ")"}, 192 | {token.SEMICOLON, ";"}, 193 | {token.EOF, ""}, 194 | } 195 | 196 | runLexerTestSuite(t, input, tests) 197 | } 198 | 199 | func TestLogicalStatements(t *testing.T) { 200 | input := 201 | ` 202 | WHERE FALSE AND three EQUAL 33; 203 | WHERE two NOT 11 OR TRUE; 204 | ` 205 | tests := []struct { 206 | expectedType token.Type 207 | expectedLiteral string 208 | }{ 209 | {token.WHERE, "WHERE"}, 210 | {token.FALSE, "FALSE"}, 211 | {token.AND, "AND"}, 212 | {token.IDENT, "three"}, 213 | {token.EQUAL, "EQUAL"}, 214 | {token.LITERAL, "33"}, 215 | {token.SEMICOLON, ";"}, 216 | {token.WHERE, "WHERE"}, 217 | {token.IDENT, "two"}, 218 | {token.NOT, "NOT"}, 219 | {token.LITERAL, "11"}, 220 | {token.OR, "OR"}, 221 | {token.TRUE, "TRUE"}, 222 | {token.SEMICOLON, ";"}, 223 | {token.EOF, ""}, 224 | } 225 | 226 | runLexerTestSuite(t, input, tests) 227 | } 228 | 229 | func TestInStatement(t *testing.T) { 230 | input := 231 | ` 232 | WHERE two IN (1, 2) AND 233 | WHERE three NOTIN ('one', 'two'); 234 | ` 235 | tests := []struct { 236 | expectedType token.Type 237 | expectedLiteral string 238 | }{ 239 | {token.WHERE, "WHERE"}, 240 | {token.IDENT, "two"}, 241 | {token.IN, "IN"}, 242 | {token.LPAREN, "("}, 243 | {token.LITERAL, "1"}, 244 | {token.COMMA, ","}, 245 | {token.LITERAL, "2"}, 246 | {token.RPAREN, ")"}, 247 | {token.AND, "AND"}, 248 | {token.WHERE, "WHERE"}, 249 | {token.IDENT, "three"}, 250 | {token.NOTIN, "NOTIN"}, 251 | {token.LPAREN, "("}, 252 | {token.APOSTROPHE, "'"}, 253 | {token.IDENT, "one"}, 254 | {token.APOSTROPHE, "'"}, 255 | {token.COMMA, ","}, 256 | {token.APOSTROPHE, "'"}, 257 | {token.IDENT, "two"}, 258 | {token.APOSTROPHE, "'"}, 259 | {token.RPAREN, ")"}, 260 | {token.SEMICOLON, ";"}, 261 | {token.EOF, ""}, 262 | } 263 | 264 | runLexerTestSuite(t, input, tests) 265 | } 266 | 267 | func TestDeleteStatement(t *testing.T) { 268 | input := `DELETE FROM table WHERE two NOT 11 OR TRUE;` 269 | tests := []struct { 270 | expectedType token.Type 271 | expectedLiteral string 272 | }{ 273 | {token.DELETE, "DELETE"}, 274 | {token.FROM, "FROM"}, 275 | {token.IDENT, "table"}, 276 | {token.WHERE, "WHERE"}, 277 | {token.IDENT, "two"}, 278 | {token.NOT, "NOT"}, 279 | {token.LITERAL, "11"}, 280 | {token.OR, "OR"}, 281 | {token.TRUE, "TRUE"}, 282 | {token.SEMICOLON, ";"}, 283 | {token.EOF, ""}, 284 | } 285 | 286 | runLexerTestSuite(t, input, tests) 287 | } 288 | 289 | func TestOrderByStatement(t *testing.T) { 290 | input := `SELECT * FROM table ORDER BY something ASC;` 291 | tests := []struct { 292 | expectedType token.Type 293 | expectedLiteral string 294 | }{ 295 | {token.SELECT, "SELECT"}, 296 | {token.ASTERISK, "*"}, 297 | {token.FROM, "FROM"}, 298 | {token.IDENT, "table"}, 299 | {token.ORDER, "ORDER"}, 300 | {token.BY, "BY"}, 301 | {token.IDENT, "something"}, 302 | {token.ASC, "ASC"}, 303 | {token.SEMICOLON, ";"}, 304 | {token.EOF, ""}, 305 | } 306 | 307 | runLexerTestSuite(t, input, tests) 308 | } 309 | 310 | func TestDropStatement(t *testing.T) { 311 | input := `DROP TABLE table;` 312 | tests := []struct { 313 | expectedType token.Type 314 | expectedLiteral string 315 | }{ 316 | {token.DROP, "DROP"}, 317 | {token.TABLE, "TABLE"}, 318 | {token.IDENT, "table"}, 319 | {token.SEMICOLON, ";"}, 320 | {token.EOF, ""}, 321 | } 322 | 323 | runLexerTestSuite(t, input, tests) 324 | } 325 | 326 | func TestLimitAndOffsetStatement(t *testing.T) { 327 | input := `LIMIT 5 OFFSET 6;` 328 | tests := []struct { 329 | expectedType token.Type 330 | expectedLiteral string 331 | }{ 332 | {token.LIMIT, "LIMIT"}, 333 | {token.LITERAL, "5"}, 334 | {token.OFFSET, "OFFSET"}, 335 | {token.LITERAL, "6"}, 336 | {token.SEMICOLON, ";"}, 337 | {token.EOF, ""}, 338 | } 339 | 340 | runLexerTestSuite(t, input, tests) 341 | } 342 | 343 | func TestAggregateFunctions(t *testing.T) { 344 | input := `SELECT MIN(colOne), MAX(colOne), COUNT(colOne), SUM(colOne), AVG(colOne) FROM tbl;` 345 | tests := []struct { 346 | expectedType token.Type 347 | expectedLiteral string 348 | }{ 349 | {token.SELECT, "SELECT"}, 350 | {token.MIN, "MIN"}, 351 | {token.LPAREN, "("}, 352 | {token.IDENT, "colOne"}, 353 | {token.RPAREN, ")"}, 354 | {token.COMMA, ","}, 355 | {token.MAX, "MAX"}, 356 | {token.LPAREN, "("}, 357 | {token.IDENT, "colOne"}, 358 | {token.RPAREN, ")"}, 359 | {token.COMMA, ","}, 360 | {token.COUNT, "COUNT"}, 361 | {token.LPAREN, "("}, 362 | {token.IDENT, "colOne"}, 363 | {token.RPAREN, ")"}, 364 | {token.COMMA, ","}, 365 | {token.SUM, "SUM"}, 366 | {token.LPAREN, "("}, 367 | {token.IDENT, "colOne"}, 368 | {token.RPAREN, ")"}, 369 | {token.COMMA, ","}, 370 | {token.AVG, "AVG"}, 371 | {token.LPAREN, "("}, 372 | {token.IDENT, "colOne"}, 373 | {token.RPAREN, ")"}, 374 | {token.FROM, "FROM"}, 375 | {token.IDENT, "tbl"}, 376 | {token.SEMICOLON, ";"}, 377 | {token.EOF, ""}, 378 | } 379 | 380 | runLexerTestSuite(t, input, tests) 381 | } 382 | 383 | func TestSelectWithDistinct(t *testing.T) { 384 | input := `SELECT DISTINCT * FROM table;` 385 | tests := []struct { 386 | expectedType token.Type 387 | expectedLiteral string 388 | }{ 389 | {token.SELECT, "SELECT"}, 390 | {token.DISTINCT, "DISTINCT"}, 391 | {token.ASTERISK, "*"}, 392 | {token.FROM, "FROM"}, 393 | {token.IDENT, "table"}, 394 | {token.SEMICOLON, ";"}, 395 | {token.EOF, ""}, 396 | } 397 | 398 | runLexerTestSuite(t, input, tests) 399 | } 400 | 401 | func TestDefaultJoin(t *testing.T) { 402 | input := ` SELECT title FROM books 403 | JOIN authors ON 404 | books.author_id EQUAL authors.author_id; 405 | ` 406 | tests := []struct { 407 | expectedType token.Type 408 | expectedLiteral string 409 | }{ 410 | {token.SELECT, "SELECT"}, 411 | {token.IDENT, "title"}, 412 | {token.FROM, "FROM"}, 413 | {token.IDENT, "books"}, 414 | {token.JOIN, "JOIN"}, 415 | {token.IDENT, "authors"}, 416 | {token.ON, "ON"}, 417 | {token.IDENT, "books.author_id"}, 418 | {token.EQUAL, "EQUAL"}, 419 | {token.IDENT, "authors.author_id"}, 420 | {token.SEMICOLON, ";"}, 421 | {token.EOF, ""}, 422 | } 423 | 424 | runLexerTestSuite(t, input, tests) 425 | } 426 | 427 | func TestInnerJoin(t *testing.T) { 428 | input := ` SELECT title FROM books 429 | INNER JOIN authors ON 430 | books.author_id EQUAL authors.author_id; 431 | ` 432 | tests := []struct { 433 | expectedType token.Type 434 | expectedLiteral string 435 | }{ 436 | {token.SELECT, "SELECT"}, 437 | {token.IDENT, "title"}, 438 | {token.FROM, "FROM"}, 439 | {token.IDENT, "books"}, 440 | {token.INNER, "INNER"}, 441 | {token.JOIN, "JOIN"}, 442 | {token.IDENT, "authors"}, 443 | {token.ON, "ON"}, 444 | {token.IDENT, "books.author_id"}, 445 | {token.EQUAL, "EQUAL"}, 446 | {token.IDENT, "authors.author_id"}, 447 | {token.SEMICOLON, ";"}, 448 | {token.EOF, ""}, 449 | } 450 | 451 | runLexerTestSuite(t, input, tests) 452 | } 453 | 454 | func TestLeftJoin(t *testing.T) { 455 | input := ` SELECT title FROM books 456 | LEFT JOIN authors ON 457 | books.author_id EQUAL authors.author_id; 458 | ` 459 | tests := []struct { 460 | expectedType token.Type 461 | expectedLiteral string 462 | }{ 463 | {token.SELECT, "SELECT"}, 464 | {token.IDENT, "title"}, 465 | {token.FROM, "FROM"}, 466 | {token.IDENT, "books"}, 467 | {token.LEFT, "LEFT"}, 468 | {token.JOIN, "JOIN"}, 469 | {token.IDENT, "authors"}, 470 | {token.ON, "ON"}, 471 | {token.IDENT, "books.author_id"}, 472 | {token.EQUAL, "EQUAL"}, 473 | {token.IDENT, "authors.author_id"}, 474 | {token.SEMICOLON, ";"}, 475 | {token.EOF, ""}, 476 | } 477 | 478 | runLexerTestSuite(t, input, tests) 479 | } 480 | 481 | func TestRightJoin(t *testing.T) { 482 | input := ` SELECT title FROM books 483 | RIGHT JOIN authors ON 484 | books.author_id EQUAL authors.author_id; 485 | ` 486 | tests := []struct { 487 | expectedType token.Type 488 | expectedLiteral string 489 | }{ 490 | {token.SELECT, "SELECT"}, 491 | {token.IDENT, "title"}, 492 | {token.FROM, "FROM"}, 493 | {token.IDENT, "books"}, 494 | {token.RIGHT, "RIGHT"}, 495 | {token.JOIN, "JOIN"}, 496 | {token.IDENT, "authors"}, 497 | {token.ON, "ON"}, 498 | {token.IDENT, "books.author_id"}, 499 | {token.EQUAL, "EQUAL"}, 500 | {token.IDENT, "authors.author_id"}, 501 | {token.SEMICOLON, ";"}, 502 | {token.EOF, ""}, 503 | } 504 | 505 | runLexerTestSuite(t, input, tests) 506 | } 507 | 508 | func TestFullJoin(t *testing.T) { 509 | input := ` SELECT title FROM books 510 | FULL JOIN authors ON 511 | books.author_id EQUAL authors.author_id; 512 | ` 513 | tests := []struct { 514 | expectedType token.Type 515 | expectedLiteral string 516 | }{ 517 | {token.SELECT, "SELECT"}, 518 | {token.IDENT, "title"}, 519 | {token.FROM, "FROM"}, 520 | {token.IDENT, "books"}, 521 | {token.FULL, "FULL"}, 522 | {token.JOIN, "JOIN"}, 523 | {token.IDENT, "authors"}, 524 | {token.ON, "ON"}, 525 | {token.IDENT, "books.author_id"}, 526 | {token.EQUAL, "EQUAL"}, 527 | {token.IDENT, "authors.author_id"}, 528 | {token.SEMICOLON, ";"}, 529 | {token.EOF, ""}, 530 | } 531 | 532 | runLexerTestSuite(t, input, tests) 533 | } 534 | 535 | func TestHandlingNullValues(t *testing.T) { 536 | input := `INSERT INTO tbl VALUES( 'NULL', NULL );` 537 | tests := []struct { 538 | expectedType token.Type 539 | expectedLiteral string 540 | }{ 541 | {token.INSERT, "INSERT"}, 542 | {token.INTO, "INTO"}, 543 | {token.IDENT, "tbl"}, 544 | {token.VALUES, "VALUES"}, 545 | {token.LPAREN, "("}, 546 | {token.APOSTROPHE, "'"}, 547 | {token.IDENT, "NULL"}, 548 | {token.APOSTROPHE, "'"}, 549 | {token.COMMA, ","}, 550 | {token.NULL, "NULL"}, 551 | {token.RPAREN, ")"}, 552 | {token.SEMICOLON, ";"}, 553 | } 554 | 555 | runLexerTestSuite(t, input, tests) 556 | } 557 | 558 | func runLexerTestSuite(t *testing.T, input string, tests []struct { 559 | expectedType token.Type 560 | expectedLiteral string 561 | }) { 562 | l := RunLexer(input) 563 | 564 | for i, tt := range tests { 565 | tok := l.NextToken() 566 | 567 | if tok.Type != tt.expectedType { 568 | t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q", 569 | i, tt.expectedType, tok.Type) 570 | } 571 | 572 | if tok.Literal != tt.expectedLiteral { 573 | t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q", 574 | i, tt.expectedLiteral, tok.Literal) 575 | } 576 | } 577 | } 578 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/LissaGreense/GO4SQL/engine" 7 | "github.com/LissaGreense/GO4SQL/modes" 8 | "log" 9 | ) 10 | 11 | func main() { 12 | filePath := flag.String("file", "", "Provide a path to the .sql file") 13 | streamMode := flag.Bool("stream", false, "Use to redirect stdin to stdout") 14 | socketMode := flag.Bool("socket", false, "Use to start socket server") 15 | port := flag.Int("port", 1433, "States on which port socket server will listen") 16 | 17 | flag.Parse() 18 | engineSQL := engine.New() 19 | var err error 20 | 21 | if len(*filePath) > 0 { 22 | err = modes.HandleFileMode(*filePath, engineSQL) 23 | } else if *streamMode { 24 | err = modes.HandleStreamMode(engineSQL) 25 | } else if *socketMode { 26 | modes.HandleSocketMode(*port, engineSQL) 27 | } else { 28 | err = fmt.Errorf("no mode has been providing, exiting") 29 | } 30 | 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modes/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package modes is responsible for handling database connection type. 3 | 4 | Currently, we support following modes: 5 | - file 6 | - stream 7 | - socket (data is not serialized, output is compatible with stream mode output) 8 | */ 9 | package modes 10 | -------------------------------------------------------------------------------- /modes/handler.go: -------------------------------------------------------------------------------- 1 | package modes 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/LissaGreense/GO4SQL/ast" 7 | "github.com/LissaGreense/GO4SQL/engine" 8 | "github.com/LissaGreense/GO4SQL/lexer" 9 | "github.com/LissaGreense/GO4SQL/parser" 10 | "log" 11 | "net" 12 | "os" 13 | "strconv" 14 | ) 15 | 16 | // HandleFileMode - Handle GO4SQL use case where client sends input via text file 17 | func HandleFileMode(filePath string, engine *engine.DbEngine) error { 18 | content, err := os.ReadFile(filePath) 19 | if err != nil { 20 | return err 21 | } 22 | sequences, err := bytesToSequences(content) 23 | if err != nil { 24 | return err 25 | } 26 | evaluate, err := engine.Evaluate(sequences) 27 | if err != nil { 28 | return err 29 | } 30 | fmt.Print(evaluate) 31 | return nil 32 | } 33 | 34 | // HandleStreamMode - Handle GO4SQL use case where client sends input via stdin 35 | func HandleStreamMode(engine *engine.DbEngine) error { 36 | reader := bufio.NewScanner(os.Stdin) 37 | for reader.Scan() { 38 | sequences, err := bytesToSequences(reader.Bytes()) 39 | if err != nil { 40 | fmt.Print(err) 41 | } else { 42 | evaluate, err := engine.Evaluate(sequences) 43 | if err != nil { 44 | fmt.Print(err) 45 | } else { 46 | fmt.Print(evaluate) 47 | } 48 | } 49 | } 50 | return reader.Err() 51 | } 52 | 53 | // HandleSocketMode - Handle GO4SQL use case where client sends input via socket protocol 54 | func HandleSocketMode(port int, engine *engine.DbEngine) { 55 | listener, err := net.Listen("tcp", "localhost:"+strconv.Itoa(port)) 56 | log.Printf("Starting Socket Server on %d port\n", port) 57 | 58 | if err != nil { 59 | log.Fatal(err.Error()) 60 | } 61 | 62 | defer func(listener net.Listener) { 63 | err := listener.Close() 64 | if err != nil { 65 | log.Fatal("Error:", err) 66 | } 67 | }(listener) 68 | 69 | for { 70 | conn, err := listener.Accept() 71 | if err != nil { 72 | fmt.Println("Error:", err) 73 | continue 74 | } 75 | 76 | go handleSocketClient(conn, engine) 77 | } 78 | } 79 | 80 | func bytesToSequences(content []byte) (*ast.Sequence, error) { 81 | lex := lexer.RunLexer(string(content)) 82 | parserInstance := parser.New(lex) 83 | sequences, err := parserInstance.ParseSequence() 84 | return sequences, err 85 | } 86 | 87 | func handleSocketClient(conn net.Conn, engine *engine.DbEngine) { 88 | defer func(conn net.Conn) { 89 | err := conn.Close() 90 | if err != nil { 91 | log.Fatal("Error:", err) 92 | } 93 | }(conn) 94 | 95 | buffer := make([]byte, 2048) 96 | 97 | for { 98 | n, err := conn.Read(buffer) 99 | if err != nil && err.Error() != "EOF" { 100 | log.Fatal(err.Error()) 101 | } 102 | sequences, err := bytesToSequences(buffer) 103 | 104 | if err != nil { 105 | log.Fatal(err.Error()) 106 | } 107 | 108 | commandResult, err := engine.Evaluate(sequences) 109 | 110 | if err != nil { 111 | _, err = conn.Write([]byte(err.Error())) 112 | } else if len(commandResult) > 0 { 113 | _, err = conn.Write([]byte(commandResult)) 114 | } 115 | 116 | if err != nil { 117 | log.Fatal(err.Error()) 118 | } 119 | 120 | log.Printf("Received: %s\n", buffer[:n]) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /parser/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package parser creates sequences from tokens 3 | */ 4 | package parser 5 | -------------------------------------------------------------------------------- /parser/errors.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | // SyntaxError - error thrown when parser was expecting different token from lexer 4 | type SyntaxError struct { 5 | expecting []string 6 | got string 7 | } 8 | 9 | func (m *SyntaxError) Error() string { 10 | var expectingText string 11 | 12 | if len(m.expecting) == 1 { 13 | expectingText = m.expecting[0] 14 | } else { 15 | for i, expected := range m.expecting { 16 | expectingText += expected 17 | if i != len(m.expecting)-1 { 18 | expectingText += ", " 19 | } 20 | } 21 | } 22 | 23 | return "syntax error, expecting: {" + expectingText + "}, got: {" + m.got + "}" 24 | } 25 | 26 | // SyntaxCommandExpectedError - error thrown when there was command that logically should only 27 | // appear after certain different command, but it wasn't found 28 | type SyntaxCommandExpectedError struct { 29 | command string 30 | neededCommands []string 31 | } 32 | 33 | func (m *SyntaxCommandExpectedError) Error() string { 34 | var neededCommandsText string 35 | 36 | if len(neededCommandsText) == 1 { 37 | neededCommandsText = m.neededCommands[0] + " command" 38 | } else if len(neededCommandsText) == 2 { 39 | neededCommandsText = m.neededCommands[0] + " or " + m.neededCommands[1] + " commands" 40 | } else { 41 | for i, command := range m.neededCommands { 42 | if i == len(m.neededCommands)-1 { 43 | neededCommandsText += " or " 44 | } 45 | 46 | neededCommandsText += command 47 | 48 | if i != len(m.neededCommands)-1 || i != len(m.neededCommands)-2 { 49 | neededCommandsText += ", " 50 | } 51 | } 52 | neededCommandsText += " commands" 53 | } 54 | 55 | return "syntax error, {" + m.command + "} command needs {" + neededCommandsText + "} before" 56 | } 57 | 58 | // SyntaxInvalidCommandError - error thrown when invalid (non-existing) type of command has been 59 | // found 60 | type SyntaxInvalidCommandError struct { 61 | invalidCommand string 62 | } 63 | 64 | func (m *SyntaxInvalidCommandError) Error() string { 65 | return "syntax error, invalid command found: {" + m.invalidCommand + "}" 66 | } 67 | 68 | // LogicalExpressionParsingError - error thrown when logical expression inside WHERE statement 69 | // couldn't be parsed correctly 70 | type LogicalExpressionParsingError struct { 71 | afterToken *string 72 | } 73 | 74 | func (m *LogicalExpressionParsingError) Error() string { 75 | errorMsg := "syntax error, logical expression within WHERE command couldn't be parsed correctly" 76 | if m.afterToken != nil { 77 | return errorMsg + ", after {" + *m.afterToken + "} character" 78 | } 79 | return errorMsg 80 | } 81 | 82 | // ArithmeticLessThanZeroParserError - error thrown when parser found integer value that shouldn't 83 | // be less than 0, but it is 84 | type ArithmeticLessThanZeroParserError struct { 85 | variable string 86 | } 87 | 88 | func (m *ArithmeticLessThanZeroParserError) Error() string { 89 | return "syntax error, {" + m.variable + "} value should be more than 0" 90 | } 91 | 92 | // NoPredecessorParserError - error thrown when parser found integer value that shouldn't 93 | // be less than 0, but it is 94 | type NoPredecessorParserError struct { 95 | command string 96 | } 97 | 98 | func (m *NoPredecessorParserError) Error() string { 99 | return "syntax error, {" + m.command + "} command can't be used without predecessor" 100 | } 101 | 102 | // IllegalPeriodInIdentParserError - error thrown when parser found period in ident when parsing create command 103 | type IllegalPeriodInIdentParserError struct { 104 | name string 105 | } 106 | 107 | func (m *IllegalPeriodInIdentParserError) Error() string { 108 | return "syntax error, {" + m.name + "} shouldn't contain '.'" 109 | } 110 | 111 | // NoApostropheOnRightParserError - error thrown when parser found no apostrophe on right of ident 112 | type NoApostropheOnRightParserError struct { 113 | ident string 114 | } 115 | 116 | func (m *NoApostropheOnRightParserError) Error() string { 117 | return "syntax error, Identifier: {" + m.ident + "} has no apostrophe on right" 118 | } 119 | 120 | // NoApostropheOnLeftParserError - error thrown when parser found no apostrophe on left of ident 121 | type NoApostropheOnLeftParserError struct { 122 | ident string 123 | } 124 | 125 | func (m *NoApostropheOnLeftParserError) Error() string { 126 | return "syntax error, Identifier: {" + m.ident + "} has no apostrophe on left" 127 | } 128 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/LissaGreense/GO4SQL/ast" 5 | "github.com/LissaGreense/GO4SQL/lexer" 6 | "github.com/LissaGreense/GO4SQL/token" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // Parser - Contain token that is currently analyzed by parser and the next one. Lexer is used to tokenize the client 12 | // text input. 13 | type Parser struct { 14 | lexer lexer.Lexer 15 | currentToken token.Token 16 | peekToken token.Token 17 | } 18 | 19 | // New - Return new Parser struct 20 | func New(lexer *lexer.Lexer) *Parser { 21 | p := &Parser{lexer: *lexer} 22 | 23 | // Read two tokens, so curToken and peekToken are both set 24 | p.nextToken() 25 | p.nextToken() 26 | 27 | return p 28 | } 29 | 30 | // nextToken - Move pointer to the next token 31 | func (parser *Parser) nextToken() { 32 | parser.currentToken = parser.peekToken 33 | parser.peekToken = parser.lexer.NextToken() 34 | } 35 | 36 | // validateTokenAndSkip - Check if current token type is appearing in provided expectedTokens array then move to the next token 37 | func validateTokenAndSkip(parser *Parser, expectedTokens []token.Type) error { 38 | err := validateToken(parser.currentToken.Type, expectedTokens) 39 | 40 | if err != nil { 41 | return err 42 | } 43 | 44 | // Ignore validated token 45 | parser.nextToken() 46 | return nil 47 | } 48 | 49 | // validateToken - Check if current token type is appearing in provided expectedTokens array 50 | func validateToken(tokenType token.Type, expectedTokens []token.Type) error { 51 | var contains = false 52 | expectedTokensStrings := make([]string, 0) 53 | for _, x := range expectedTokens { 54 | expectedTokensStrings = append(expectedTokensStrings, string(x)) 55 | 56 | if x == tokenType { 57 | contains = true 58 | break 59 | } 60 | } 61 | if !contains { 62 | return &SyntaxError{expectedTokensStrings, string(tokenType)} 63 | } 64 | return nil 65 | } 66 | 67 | // parseCreateCommand - Return ast.CreateCommand created from tokens and validate the syntax 68 | // 69 | // Example of input parsable to the ast.CreateCommand: 70 | // create table tbl( one TEXT , two INT ); 71 | func (parser *Parser) parseCreateCommand() (ast.Command, error) { 72 | // token.CREATE already at current position in parser 73 | createCommand := &ast.CreateCommand{Token: parser.currentToken} 74 | 75 | // Skip token.CREATE 76 | parser.nextToken() 77 | 78 | err := validateTokenAndSkip(parser, []token.Type{token.TABLE}) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | if strings.Contains(parser.currentToken.Literal, ".") { 89 | return nil, &IllegalPeriodInIdentParserError{name: parser.currentToken.Literal} 90 | } 91 | createCommand.Name = ast.Identifier{Token: parser.currentToken} 92 | 93 | // Skip token.IDENT 94 | parser.nextToken() 95 | 96 | err = validateTokenAndSkip(parser, []token.Type{token.LPAREN}) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | // Begin of inside Paren 102 | for parser.currentToken.Type == token.IDENT { 103 | err = validateToken(parser.peekToken.Type, []token.Type{token.TEXT, token.INT}) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | if strings.Contains(parser.currentToken.Literal, ".") { 109 | return nil, &IllegalPeriodInIdentParserError{name: parser.currentToken.Literal} 110 | } 111 | 112 | createCommand.ColumnNames = append(createCommand.ColumnNames, parser.currentToken.Literal) 113 | createCommand.ColumnTypes = append(createCommand.ColumnTypes, parser.peekToken) 114 | 115 | // Skip token.IDENT 116 | parser.nextToken() 117 | // Skip token.TEXT or token.INT 118 | parser.nextToken() 119 | 120 | if parser.currentToken.Type != token.COMMA { 121 | break 122 | } 123 | 124 | // Skip token.COMMA 125 | parser.nextToken() 126 | } 127 | // End of inside Paren 128 | 129 | err = validateTokenAndSkip(parser, []token.Type{token.RPAREN}) 130 | if err != nil { 131 | return nil, err 132 | } 133 | err = validateTokenAndSkip(parser, []token.Type{token.SEMICOLON}) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | return createCommand, nil 139 | } 140 | 141 | func (parser *Parser) skipIfCurrentTokenIsApostrophe() bool { 142 | if parser.currentToken.Type == token.APOSTROPHE { 143 | parser.nextToken() 144 | return true 145 | } 146 | return false 147 | } 148 | 149 | func (parser *Parser) skipIfCurrentTokenIsSemicolon() { 150 | if parser.currentToken.Type == token.SEMICOLON { 151 | parser.nextToken() 152 | } 153 | } 154 | 155 | // parseInsertCommand - Return ast.InsertCommand created from tokens and validate the syntax 156 | // 157 | // Example of input parsable to the ast.InsertCommand: 158 | // insert into tbl values( 'hello', 10 ); 159 | func (parser *Parser) parseInsertCommand() (ast.Command, error) { 160 | // token.INSERT already at current position in parser 161 | insertCommand := &ast.InsertCommand{Token: parser.currentToken} 162 | 163 | // Ignore token.INSERT 164 | parser.nextToken() 165 | 166 | err := validateTokenAndSkip(parser, []token.Type{token.INTO}) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 172 | if err != nil { 173 | return nil, err 174 | } 175 | insertCommand.Name = ast.Identifier{Token: parser.currentToken} 176 | // Ignore token.INDENT 177 | parser.nextToken() 178 | 179 | err = validateTokenAndSkip(parser, []token.Type{token.VALUES}) 180 | if err != nil { 181 | return nil, err 182 | } 183 | err = validateTokenAndSkip(parser, []token.Type{token.LPAREN}) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | for parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { 189 | startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 190 | 191 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.LITERAL, token.NULL}) 192 | if err != nil { 193 | return nil, err 194 | } 195 | value := parser.currentToken 196 | insertCommand.Values = append(insertCommand.Values, value) 197 | // Ignore token.IDENT, token.LITERAL or token.NULL 198 | parser.nextToken() 199 | 200 | finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 201 | 202 | err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, value) 203 | 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | if parser.currentToken.Type != token.COMMA { 209 | break 210 | } 211 | 212 | // Ignore token.COMMA 213 | parser.nextToken() 214 | } 215 | 216 | err = validateTokenAndSkip(parser, []token.Type{token.RPAREN}) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | err = validateTokenAndSkip(parser, []token.Type{token.SEMICOLON}) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | return insertCommand, nil 227 | } 228 | 229 | func validateApostropheWrapping(startedWithApostrophe bool, finishedWithApostrophe bool, value token.Token) error { 230 | if startedWithApostrophe && !finishedWithApostrophe { 231 | return &NoApostropheOnRightParserError{ident: value.Literal} 232 | } else if !startedWithApostrophe && finishedWithApostrophe { 233 | return &NoApostropheOnLeftParserError{ident: value.Literal} 234 | } 235 | return nil 236 | } 237 | 238 | // parseSelectCommand - Return ast.SelectCommand created from tokens and validate the syntax 239 | // 240 | // Example of input parsable to the ast.SelectCommand: 241 | // SELECT col1, col2, col3 FROM tbl; 242 | func (parser *Parser) parseSelectCommand() (ast.Command, error) { 243 | // token.SELECT already at current position in parser 244 | selectCommand := &ast.SelectCommand{Token: parser.currentToken} 245 | 246 | // Ignore token.SELECT 247 | parser.nextToken() 248 | 249 | // optional DISTINCT 250 | if parser.currentToken.Type == token.DISTINCT { 251 | selectCommand.HasDistinct = true 252 | 253 | // Ignore token.DISTINCT 254 | parser.nextToken() 255 | } 256 | 257 | err := validateToken(parser.currentToken.Type, []token.Type{token.ASTERISK, token.IDENT, token.MAX, token.MIN, token.SUM, token.AVG, token.COUNT}) 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | if parser.currentToken.Type == token.ASTERISK { 263 | selectCommand.Space = append(selectCommand.Space, ast.Space{ColumnName: parser.currentToken}) 264 | parser.nextToken() 265 | } else { 266 | command, err := parser.parseSelectSpace(selectCommand) 267 | if err != nil { 268 | return command, err 269 | } 270 | } 271 | 272 | err = validateTokenAndSkip(parser, []token.Type{token.FROM}) 273 | if err != nil { 274 | return nil, err 275 | } 276 | 277 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 278 | if err != nil { 279 | return nil, err 280 | } 281 | 282 | selectCommand.Name = ast.Identifier{Token: parser.currentToken} 283 | // Ignore token.IDENT 284 | parser.nextToken() 285 | 286 | // expect SEMICOLON or other keywords expected in SELECT statement 287 | err = validateToken(parser.currentToken.Type, []token.Type{token.SEMICOLON, token.WHERE, token.ORDER, token.LIMIT, token.OFFSET, token.JOIN, token.LEFT, token.RIGHT, token.INNER, token.FULL}) 288 | if err != nil { 289 | return nil, err 290 | } 291 | 292 | if parser.currentToken.Type == token.SEMICOLON { 293 | parser.nextToken() 294 | } 295 | 296 | return selectCommand, nil 297 | } 298 | 299 | func (parser *Parser) parseSelectSpace(selectCommand *ast.SelectCommand) (ast.Command, error) { 300 | for parser.currentToken.Type == token.IDENT || isAggregateFunction(parser.currentToken.Type) { 301 | if parser.currentToken.Type != token.IDENT { 302 | err := parser.parseAggregateFunction(selectCommand) 303 | if err != nil { 304 | return nil, err 305 | } 306 | } else { 307 | // Get column name 308 | err := validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 309 | if err != nil { 310 | return nil, err 311 | } 312 | selectCommand.Space = append(selectCommand.Space, ast.Space{ColumnName: parser.currentToken}) 313 | parser.nextToken() 314 | } 315 | 316 | if parser.currentToken.Type != token.COMMA { 317 | break 318 | } 319 | // Ignore token.COMMA 320 | parser.nextToken() 321 | } 322 | return nil, nil 323 | } 324 | 325 | func (parser *Parser) parseAggregateFunction(selectCommand *ast.SelectCommand) error { 326 | aggregateFunction := parser.currentToken 327 | parser.nextToken() 328 | err := validateTokenAndSkip(parser, []token.Type{token.LPAREN}) 329 | if err != nil { 330 | return err 331 | } 332 | if aggregateFunction.Type == token.COUNT { 333 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.ASTERISK}) 334 | } else { 335 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 336 | } 337 | if err != nil { 338 | return err 339 | } 340 | selectCommand.Space = append(selectCommand.Space, ast.Space{ColumnName: parser.currentToken, AggregateFunc: &aggregateFunction}) 341 | parser.nextToken() 342 | 343 | err = validateTokenAndSkip(parser, []token.Type{token.RPAREN}) 344 | return err 345 | } 346 | 347 | func (parser *Parser) getColumnName(err error, selectCommand *ast.SelectCommand, aggregateFunction token.Token) error { 348 | // Get column name 349 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.ASTERISK}) 350 | if err != nil { 351 | return err 352 | } 353 | selectCommand.Space = append(selectCommand.Space, ast.Space{ColumnName: parser.currentToken, AggregateFunc: &aggregateFunction}) 354 | parser.nextToken() 355 | return nil 356 | } 357 | 358 | func isAggregateFunction(t token.Type) bool { 359 | return t == token.MIN || t == token.MAX || t == token.COUNT || t == token.SUM || t == token.AVG 360 | } 361 | 362 | // parseWhereCommand - Return ast.WhereCommand created from tokens and validate the syntax 363 | // 364 | // Example of input parsable to the ast.WhereCommand: 365 | // WHERE colName EQUAL 'potato' 366 | func (parser *Parser) parseWhereCommand() (ast.Command, error) { 367 | // token.WHERE already at current position in parser 368 | whereCommand := &ast.WhereCommand{Token: parser.currentToken} 369 | 370 | // Ignore token.WHERE 371 | parser.nextToken() 372 | var err error 373 | whereCommand.Expression, err = parser.getExpression() 374 | if err != nil { 375 | return nil, err 376 | } 377 | 378 | if whereCommand.Expression == nil { 379 | return nil, &LogicalExpressionParsingError{} 380 | } 381 | 382 | err = validateToken(parser.currentToken.Type, []token.Type{token.SEMICOLON, token.ORDER}) 383 | if err != nil { 384 | return nil, err 385 | } 386 | 387 | parser.skipIfCurrentTokenIsSemicolon() 388 | 389 | return whereCommand, nil 390 | } 391 | 392 | // parseDeleteCommand - Return ast.DeleteCommand created from tokens and validate the syntax 393 | // 394 | // Example of input parsable to the ast.DeleteCommand: 395 | // DELETE FROM table; 396 | func (parser *Parser) parseDeleteCommand() (ast.Command, error) { 397 | // token.DELETE already at current position in parser 398 | deleteCommand := &ast.DeleteCommand{Token: parser.currentToken} 399 | 400 | // token.DELETE no longer needed 401 | parser.nextToken() 402 | 403 | err := validateTokenAndSkip(parser, []token.Type{token.FROM}) 404 | if err != nil { 405 | return nil, err 406 | } 407 | 408 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 409 | if err != nil { 410 | return nil, err 411 | } 412 | deleteCommand.Name = ast.Identifier{Token: parser.currentToken} 413 | 414 | // token.IDENT no longer needed 415 | parser.nextToken() 416 | 417 | // expect WHERE 418 | err = validateToken(parser.currentToken.Type, []token.Type{token.WHERE}) 419 | 420 | return deleteCommand, err 421 | } 422 | 423 | // parseDropCommand - Return ast.DropCommand created from tokens and validate the syntax 424 | // 425 | // Example of input parsable to the ast.DropCommand: 426 | // DROP TABLE table; 427 | func (parser *Parser) parseDropCommand() (ast.Command, error) { 428 | // token.DROP already at current position in parser 429 | dropCommand := &ast.DropCommand{Token: parser.currentToken} 430 | 431 | // token.DROP no longer needed 432 | parser.nextToken() 433 | 434 | err := validateTokenAndSkip(parser, []token.Type{token.TABLE}) 435 | if err != nil { 436 | return nil, err 437 | } 438 | 439 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 440 | if err != nil { 441 | return nil, err 442 | } 443 | dropCommand.Name = ast.Identifier{Token: parser.currentToken} 444 | 445 | // token.IDENT no longer needed 446 | parser.nextToken() 447 | 448 | err = validateTokenAndSkip(parser, []token.Type{token.SEMICOLON}) 449 | 450 | return dropCommand, err 451 | } 452 | 453 | // parseOrderByCommand - Return ast.OrderByCommand created from tokens and validate the syntax 454 | // 455 | // Example of input parsable to the ast.OrderByCommand: 456 | // ORDER BY colName ASC 457 | func (parser *Parser) parseOrderByCommand() (ast.Command, error) { 458 | // token.ORDER already at current position in parser 459 | orderCommand := &ast.OrderByCommand{Token: parser.currentToken} 460 | 461 | // token.ORDER no longer needed 462 | parser.nextToken() 463 | 464 | err := validateTokenAndSkip(parser, []token.Type{token.BY}) 465 | if err != nil { 466 | return nil, err 467 | } 468 | 469 | // ensure that loop below will execute at least once 470 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 471 | if err != nil { 472 | return nil, err 473 | } 474 | 475 | // array of SortPattern 476 | for parser.currentToken.Type == token.IDENT { 477 | // Get column name 478 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 479 | if err != nil { 480 | return nil, err 481 | } 482 | columnName := parser.currentToken 483 | parser.nextToken() 484 | 485 | // Get ASC or DESC 486 | err = validateToken(parser.currentToken.Type, []token.Type{token.ASC, token.DESC}) 487 | if err != nil { 488 | return nil, err 489 | } 490 | order := parser.currentToken 491 | parser.nextToken() 492 | 493 | // append sortPattern 494 | orderCommand.SortPatterns = append(orderCommand.SortPatterns, ast.SortPattern{ColumnName: columnName, Order: order}) 495 | 496 | if parser.currentToken.Type != token.COMMA { 497 | break 498 | } 499 | // Ignore token.COMMA 500 | parser.nextToken() 501 | } 502 | 503 | parser.skipIfCurrentTokenIsSemicolon() 504 | 505 | return orderCommand, nil 506 | } 507 | 508 | // parseLimitCommand - Return ast.LimitCommand created from tokens and validate the syntax 509 | // 510 | // Example of input parsable to the ast.LimitCommand: 511 | // LIMIT 10 512 | func (parser *Parser) parseLimitCommand() (ast.Command, error) { 513 | // token.LIMIT already at current position in parser 514 | limitCommand := &ast.LimitCommand{Token: parser.currentToken} 515 | 516 | // token.LIMIT no longer needed 517 | parser.nextToken() 518 | 519 | err := validateToken(parser.currentToken.Type, []token.Type{token.LITERAL}) 520 | if err != nil { 521 | return nil, err 522 | } 523 | 524 | // convert count number to int 525 | count, err := strconv.Atoi(parser.currentToken.Literal) 526 | if err != nil { 527 | return nil, err 528 | } 529 | 530 | if count < 0 { 531 | return nil, &ArithmeticLessThanZeroParserError{variable: "limit"} 532 | } 533 | 534 | limitCommand.Count = count 535 | 536 | // Skip token.IDENT 537 | parser.nextToken() 538 | 539 | parser.skipIfCurrentTokenIsSemicolon() 540 | 541 | return limitCommand, nil 542 | } 543 | 544 | // parseOffsetCommand - Return ast.OffsetCommand created from tokens and validate the syntax 545 | // 546 | // Example of input parsable to the ast.LimitCommand: 547 | // OFFSET 10 548 | func (parser *Parser) parseOffsetCommand() (ast.Command, error) { 549 | // token.OFFSET already at current position in parser 550 | offsetCommand := &ast.OffsetCommand{Token: parser.currentToken} 551 | 552 | // token.OFFSET no longer needed 553 | parser.nextToken() 554 | 555 | err := validateToken(parser.currentToken.Type, []token.Type{token.LITERAL}) 556 | if err != nil { 557 | return nil, err 558 | } 559 | 560 | count, err := strconv.Atoi(parser.currentToken.Literal) 561 | if err != nil { 562 | return nil, err 563 | } 564 | if count < 0 { 565 | return nil, &ArithmeticLessThanZeroParserError{variable: "offset"} 566 | } 567 | 568 | offsetCommand.Count = count 569 | 570 | // Skip token.IDENT 571 | parser.nextToken() 572 | 573 | parser.skipIfCurrentTokenIsSemicolon() 574 | 575 | return offsetCommand, nil 576 | } 577 | 578 | // parseJoinCommand - Return ast.JoinCommand created from tokens and validate the syntax 579 | // 580 | // Example of input parsable to the ast.JoinCommand: 581 | // JOIN table on table.one EQUAL table2.one; 582 | func (parser *Parser) parseJoinCommand() (ast.Command, error) { 583 | // parser has either token.JOIN, token.LEFT, token.RIGHT, token.INNER or token.FULL 584 | var joinCommand *ast.JoinCommand 585 | 586 | if parser.currentToken.Type == token.JOIN { 587 | joinCommand = &ast.JoinCommand{Token: parser.currentToken} 588 | joinCommand.JoinType = token.Token{Type: token.INNER, Literal: token.INNER} 589 | } else { 590 | joinTypeTokenType := parser.currentToken 591 | parser.nextToken() 592 | err := validateToken(parser.currentToken.Type, []token.Type{token.JOIN}) 593 | if err != nil { 594 | return nil, err 595 | } 596 | joinCommand = &ast.JoinCommand{Token: parser.currentToken} 597 | joinCommand.JoinType = joinTypeTokenType 598 | } 599 | 600 | // token.JOIN no longer needed 601 | parser.nextToken() 602 | 603 | err := validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 604 | if err != nil { 605 | return nil, err 606 | } 607 | 608 | joinCommand.Name = ast.Identifier{Token: parser.currentToken} 609 | parser.nextToken() 610 | 611 | err = validateTokenAndSkip(parser, []token.Type{token.ON}) 612 | if err != nil { 613 | return nil, err 614 | } 615 | 616 | joinCommand.Expression, err = parser.getExpression() 617 | if err != nil { 618 | return nil, err 619 | } 620 | 621 | if joinCommand.Expression == nil { 622 | return nil, &LogicalExpressionParsingError{} 623 | } 624 | 625 | parser.skipIfCurrentTokenIsSemicolon() 626 | 627 | return joinCommand, nil 628 | } 629 | 630 | // parseUpdateCommand - Return ast.parseUpdateCommand created from tokens and validate the syntax 631 | // 632 | // Example of input parsable to the ast.parseUpdateCommand: 633 | // UPDATE table SET col1 TO 'value' WHERE col2 EQUAL 10; 634 | func (parser *Parser) parseUpdateCommand() (ast.Command, error) { 635 | // token.UPDATE already at current position in parser 636 | updateCommand := &ast.UpdateCommand{Token: parser.currentToken} 637 | 638 | // Ignore token.UPDATE 639 | parser.nextToken() 640 | 641 | // Get table name 642 | err := validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 643 | if err != nil { 644 | return nil, err 645 | } 646 | updateCommand.Name = ast.Identifier{Token: parser.currentToken} 647 | 648 | // Ignore token.IDENT 649 | parser.nextToken() 650 | 651 | err = validateTokenAndSkip(parser, []token.Type{token.SET}) 652 | if err != nil { 653 | return nil, err 654 | } 655 | 656 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 657 | if err != nil { 658 | return nil, err 659 | } 660 | 661 | updateCommand.Changes = make(map[token.Token]ast.Anonymitifier) 662 | for parser.currentToken.Type == token.IDENT { 663 | // Get column name 664 | err := validateToken(parser.currentToken.Type, []token.Type{token.IDENT}) 665 | if err != nil { 666 | return nil, err 667 | } 668 | colKey := parser.currentToken 669 | 670 | // skip column name 671 | parser.nextToken() 672 | 673 | err = validateToken(parser.currentToken.Type, []token.Type{token.TO}) 674 | if err != nil { 675 | return nil, err 676 | } 677 | // skip token.TO 678 | parser.nextToken() 679 | 680 | startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 681 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.LITERAL, token.NULL}) 682 | if err != nil { 683 | return nil, err 684 | } 685 | updateCommand.Changes[colKey] = ast.Anonymitifier{Token: parser.currentToken} 686 | 687 | // skip token.IDENT, token.LITERAL or token.NULL 688 | parser.nextToken() 689 | finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 690 | 691 | err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, updateCommand.Changes[colKey].GetToken()) 692 | 693 | if err != nil { 694 | return nil, err 695 | } 696 | 697 | if parser.currentToken.Type != token.COMMA { 698 | break 699 | } 700 | 701 | // Skip token.COMMA 702 | parser.nextToken() 703 | } 704 | 705 | err = validateToken(parser.currentToken.Type, []token.Type{token.SEMICOLON, token.WHERE}) 706 | if err != nil { 707 | return nil, err 708 | } 709 | parser.skipIfCurrentTokenIsSemicolon() 710 | return updateCommand, nil 711 | } 712 | 713 | // getExpression - Return proper structure of ast.Expression and validate the syntax 714 | // 715 | // Available expressions: 716 | // - ast.OperationExpression 717 | // - ast.BooleanExpression 718 | // - ast.ConditionExpression 719 | // - ast.ContainExpression 720 | func (parser *Parser) getExpression() (ast.Expression, error) { 721 | 722 | if parser.currentToken.Type == token.IDENT || 723 | parser.currentToken.Type == token.LITERAL || 724 | parser.currentToken.Type == token.NULL || 725 | parser.currentToken.Type == token.APOSTROPHE || 726 | parser.currentToken.Type == token.TRUE || 727 | parser.currentToken.Type == token.FALSE { 728 | 729 | leftSide, isAnonymitifier, err := parser.getExpressionLeftSideValue() 730 | if err != nil { 731 | return nil, err 732 | } 733 | 734 | expression, err := parser.getExpressionLeaf(leftSide, isAnonymitifier) 735 | 736 | if err != nil { 737 | return nil, err 738 | } 739 | 740 | if (parser.currentToken.Type == token.AND || parser.currentToken.Type == token.OR) && expression != nil { 741 | expression, err = parser.getOperationExpression(expression) 742 | } 743 | 744 | if err != nil { 745 | return nil, err 746 | } 747 | 748 | if expression != nil { 749 | return expression, nil 750 | } 751 | } 752 | return nil, nil 753 | } 754 | 755 | func (parser *Parser) getExpressionLeaf(leftSide token.Token, isAnonymitifier bool) (ast.Expression, error) { 756 | if parser.currentToken.Type == token.EQUAL || parser.currentToken.Type == token.NOT { 757 | return parser.getConditionalExpression(leftSide, isAnonymitifier) 758 | } else if parser.currentToken.Type == token.IN || parser.currentToken.Type == token.NOTIN { 759 | return parser.getContainExpression(leftSide, isAnonymitifier) 760 | } else if leftSide.Type == token.TRUE || leftSide.Type == token.FALSE { 761 | return &ast.BooleanExpression{Boolean: leftSide}, nil 762 | } 763 | return nil, nil 764 | } 765 | 766 | func (parser *Parser) getExpressionLeftSideValue() (token.Token, bool, error) { 767 | var leftSide token.Token 768 | isAnonymitifier := false 769 | startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 770 | 771 | if startedWithApostrophe { 772 | isAnonymitifier = true 773 | value := "" 774 | for parser.currentToken.Type != token.EOF && parser.currentToken.Type != token.APOSTROPHE { 775 | value += parser.currentToken.Literal 776 | parser.nextToken() 777 | } 778 | 779 | leftSide = token.Token{Type: token.IDENT, Literal: value} 780 | 781 | finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 782 | 783 | err := validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, leftSide) 784 | 785 | if err != nil { 786 | return token.Token{}, isAnonymitifier, err 787 | } 788 | } else { 789 | leftSide = parser.currentToken 790 | parser.nextToken() 791 | } 792 | return leftSide, isAnonymitifier, nil 793 | } 794 | 795 | // getOperationExpression - Return ast.OperationExpression created from tokens and validate the syntax 796 | func (parser *Parser) getOperationExpression(expression ast.Expression) (*ast.OperationExpression, error) { 797 | operationExpression := &ast.OperationExpression{} 798 | operationExpression.Left = expression 799 | 800 | operationExpression.Operation = parser.currentToken 801 | parser.nextToken() 802 | 803 | expression, err := parser.getExpression() 804 | 805 | if err != nil { 806 | return nil, err 807 | } 808 | 809 | if expression == nil { 810 | return nil, &LogicalExpressionParsingError{afterToken: &operationExpression.Operation.Literal} 811 | } 812 | 813 | operationExpression.Right = expression 814 | 815 | return operationExpression, nil 816 | } 817 | 818 | // getConditionalExpression - Return ast.ConditionExpression created from tokens and validate the syntax 819 | func (parser *Parser) getConditionalExpression(leftSide token.Token, isAnonymitifier bool) (*ast.ConditionExpression, error) { 820 | conditionalExpression := &ast.ConditionExpression{Condition: parser.currentToken} 821 | 822 | if isAnonymitifier { 823 | conditionalExpression.Left = ast.Anonymitifier{Token: leftSide} 824 | } else { 825 | conditionalExpression.Left = ast.Identifier{Token: leftSide} 826 | } 827 | 828 | // skip EQUAL or NOT 829 | parser.nextToken() 830 | 831 | if parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || 832 | parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { 833 | startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 834 | 835 | if !startedWithApostrophe && parser.currentToken.Type == token.IDENT { 836 | conditionalExpression.Right = ast.Identifier{Token: parser.currentToken} 837 | } else { 838 | conditionalExpression.Right = ast.Anonymitifier{Token: parser.currentToken} 839 | } 840 | parser.nextToken() 841 | 842 | finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 843 | err := validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, conditionalExpression.Right.GetToken()) 844 | if err != nil { 845 | return nil, err 846 | } 847 | } else { 848 | return nil, &SyntaxError{expecting: []string{token.APOSTROPHE, token.IDENT, token.LITERAL, token.NULL}, got: parser.currentToken.Literal} 849 | } 850 | 851 | return conditionalExpression, nil 852 | } 853 | 854 | // getContainExpression - Return ast.ContainExpression created from tokens and validate the syntax 855 | func (parser *Parser) getContainExpression(leftSide token.Token, isAnonymitifier bool) (*ast.ContainExpression, error) { 856 | containExpression := &ast.ContainExpression{} 857 | 858 | if isAnonymitifier { 859 | return nil, &SyntaxError{expecting: []string{token.IDENT}, got: "'" + leftSide.Literal + "'"} 860 | } 861 | 862 | containExpression.Left = ast.Identifier{Token: leftSide} 863 | 864 | if parser.currentToken.Type == token.IN { 865 | containExpression.Contains = true 866 | } else { 867 | containExpression.Contains = false 868 | } 869 | 870 | // skip IN or NOTIN 871 | parser.nextToken() 872 | 873 | err := validateTokenAndSkip(parser, []token.Type{token.LPAREN}) 874 | if err != nil { 875 | return nil, err 876 | } 877 | 878 | for parser.currentToken.Type == token.IDENT || parser.currentToken.Type == token.LITERAL || parser.currentToken.Type == token.NULL || parser.currentToken.Type == token.APOSTROPHE { 879 | startedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 880 | 881 | err = validateToken(parser.currentToken.Type, []token.Type{token.IDENT, token.LITERAL, token.NULL}) 882 | if err != nil { 883 | return nil, err 884 | } 885 | currentAnonymitifier := ast.Anonymitifier{Token: parser.currentToken} 886 | containExpression.Right = append(containExpression.Right, currentAnonymitifier) 887 | // Ignore token.IDENT, token.LITERAL or token.NULL 888 | parser.nextToken() 889 | 890 | finishedWithApostrophe := parser.skipIfCurrentTokenIsApostrophe() 891 | 892 | err = validateApostropheWrapping(startedWithApostrophe, finishedWithApostrophe, currentAnonymitifier.GetToken()) 893 | if err != nil { 894 | return nil, err 895 | } 896 | 897 | if parser.currentToken.Type != token.COMMA { 898 | if parser.currentToken.Type != token.RPAREN { 899 | return nil, &SyntaxError{expecting: []string{token.COMMA, token.RPAREN}, got: string(parser.currentToken.Type)} 900 | } 901 | break 902 | } 903 | 904 | // Ignore token.COMMA 905 | parser.nextToken() 906 | } 907 | 908 | err = validateTokenAndSkip(parser, []token.Type{token.RPAREN}) 909 | if err != nil { 910 | return nil, err 911 | } 912 | 913 | return containExpression, err 914 | } 915 | 916 | // ParseSequence - Return ast.Sequence (sequence of commands) created from client input after tokenization 917 | // 918 | // Parse tokens returned by lexer to structures defines in ast package, and it's responsible for syntax validation. 919 | func (parser *Parser) ParseSequence() (*ast.Sequence, error) { 920 | // Create variable holding sequence/commands 921 | sequence := &ast.Sequence{} 922 | 923 | for parser.currentToken.Type != token.EOF { 924 | var command ast.Command 925 | var err error 926 | switch parser.currentToken.Type { 927 | case token.CREATE: 928 | command, err = parser.parseCreateCommand() 929 | case token.INSERT: 930 | command, err = parser.parseInsertCommand() 931 | case token.UPDATE: 932 | command, err = parser.parseUpdateCommand() 933 | case token.SELECT: 934 | command, err = parser.parseSelectCommand() 935 | case token.DELETE: 936 | command, err = parser.parseDeleteCommand() 937 | case token.DROP: 938 | command, err = parser.parseDropCommand() 939 | case token.WHERE: 940 | err = parser.updateLastCommandWithWhereConstraints(sequence) 941 | case token.ORDER: 942 | err = parser.updateSelectCommandWithOrderByConstraints(sequence) 943 | case token.LIMIT: 944 | err = parser.updateSelectCommandWithLimitConstraints(sequence) 945 | case token.OFFSET: 946 | err = parser.updateSelectCommandWithOffsetConstraints(sequence) 947 | case token.JOIN, token.LEFT, token.RIGHT, token.INNER, token.FULL: 948 | err = parser.updateSelectCommandWithJoinConstraints(sequence) 949 | default: 950 | return nil, &SyntaxInvalidCommandError{invalidCommand: parser.currentToken.Literal} 951 | } 952 | 953 | if err != nil { 954 | return nil, err 955 | } 956 | 957 | // Add command to the list of parsed commands 958 | if command != nil { 959 | sequence.Commands = append(sequence.Commands, command) 960 | } 961 | } 962 | return sequence, nil 963 | } 964 | 965 | func (parser *Parser) updateSelectCommandWithJoinConstraints(sequence *ast.Sequence) error { 966 | lastCommand, parserError := parser.getLastCommand(sequence, token.JOIN) 967 | if parserError != nil { 968 | return parserError 969 | } 970 | if lastCommand.TokenLiteral() != token.SELECT { 971 | return &SyntaxCommandExpectedError{command: "JOIN", neededCommands: []string{"SELECT"}} 972 | } 973 | selectCommand := lastCommand.(*ast.SelectCommand) 974 | newCommand, err := parser.parseJoinCommand() 975 | if err != nil { 976 | return err 977 | } 978 | selectCommand.JoinCommand = newCommand.(*ast.JoinCommand) 979 | return nil 980 | } 981 | 982 | func (parser *Parser) updateSelectCommandWithOffsetConstraints(sequence *ast.Sequence) error { 983 | lastCommand, parserError := parser.getLastCommand(sequence, token.OFFSET) 984 | if parserError != nil { 985 | return parserError 986 | } 987 | if lastCommand.TokenLiteral() != token.SELECT { 988 | return &SyntaxCommandExpectedError{command: "OFFSET", neededCommands: []string{"SELECT"}} 989 | } 990 | selectCommand := lastCommand.(*ast.SelectCommand) 991 | newCommand, err := parser.parseOffsetCommand() 992 | if err != nil { 993 | return err 994 | } 995 | selectCommand.OffsetCommand = newCommand.(*ast.OffsetCommand) 996 | return nil 997 | } 998 | 999 | func (parser *Parser) updateSelectCommandWithLimitConstraints(sequence *ast.Sequence) error { 1000 | lastCommand, parserError := parser.getLastCommand(sequence, token.LIMIT) 1001 | if parserError != nil { 1002 | return parserError 1003 | } 1004 | if lastCommand.TokenLiteral() != token.SELECT { 1005 | return &SyntaxCommandExpectedError{command: "LIMIT", neededCommands: []string{"SELECT"}} 1006 | } 1007 | selectCommand := lastCommand.(*ast.SelectCommand) 1008 | newCommand, err := parser.parseLimitCommand() 1009 | if err != nil { 1010 | return err 1011 | } 1012 | selectCommand.LimitCommand = newCommand.(*ast.LimitCommand) 1013 | return nil 1014 | } 1015 | 1016 | func (parser *Parser) updateSelectCommandWithOrderByConstraints(sequence *ast.Sequence) error { 1017 | lastCommand, parserError := parser.getLastCommand(sequence, token.ORDER) 1018 | if parserError != nil { 1019 | return parserError 1020 | } 1021 | 1022 | if lastCommand.TokenLiteral() != token.SELECT { 1023 | return &SyntaxCommandExpectedError{command: "ORDER BY", neededCommands: []string{"SELECT"}} 1024 | } 1025 | 1026 | selectCommand := lastCommand.(*ast.SelectCommand) 1027 | newCommand, err := parser.parseOrderByCommand() 1028 | if err != nil { 1029 | return err 1030 | } 1031 | selectCommand.OrderByCommand = newCommand.(*ast.OrderByCommand) 1032 | return nil 1033 | } 1034 | 1035 | func (parser *Parser) updateLastCommandWithWhereConstraints(sequence *ast.Sequence) error { 1036 | lastCommand, parserError := parser.getLastCommand(sequence, token.WHERE) 1037 | if parserError != nil { 1038 | return parserError 1039 | } 1040 | 1041 | if lastCommand.TokenLiteral() == token.SELECT { 1042 | newCommand, err := parser.parseWhereCommand() 1043 | if err != nil { 1044 | return err 1045 | } 1046 | lastCommand.(*ast.SelectCommand).WhereCommand = newCommand.(*ast.WhereCommand) 1047 | } else if lastCommand.TokenLiteral() == token.DELETE { 1048 | newCommand, err := parser.parseWhereCommand() 1049 | if err != nil { 1050 | return err 1051 | } 1052 | lastCommand.(*ast.DeleteCommand).WhereCommand = newCommand.(*ast.WhereCommand) 1053 | } else if lastCommand.TokenLiteral() == token.UPDATE { 1054 | newCommand, err := parser.parseWhereCommand() 1055 | if err != nil { 1056 | return err 1057 | } 1058 | lastCommand.(*ast.UpdateCommand).WhereCommand = newCommand.(*ast.WhereCommand) 1059 | } else { 1060 | return &SyntaxCommandExpectedError{command: "WHERE", neededCommands: []string{"SELECT", "DELETE", "UPDATE"}} 1061 | } 1062 | return nil 1063 | } 1064 | 1065 | func (parser *Parser) getLastCommand(sequence *ast.Sequence, currentToken string) (ast.Command, error) { 1066 | if len(sequence.Commands) == 0 { 1067 | return nil, &NoPredecessorParserError{command: currentToken} 1068 | } 1069 | lastCommand := sequence.Commands[len(sequence.Commands)-1] 1070 | return lastCommand, nil 1071 | } 1072 | -------------------------------------------------------------------------------- /parser/parser_error_handling_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/LissaGreense/GO4SQL/lexer" 5 | "github.com/LissaGreense/GO4SQL/token" 6 | "testing" 7 | ) 8 | 9 | type errorHandlingTestSuite struct { 10 | input string 11 | expectedError string 12 | } 13 | 14 | func TestParseCreateCommandErrorHandling(t *testing.T) { 15 | noTableKeyword := SyntaxError{[]string{token.TABLE}, token.IDENT} 16 | noTableName := SyntaxError{[]string{token.IDENT}, token.LPAREN} 17 | noLeftParen := SyntaxError{[]string{token.LPAREN}, token.IDENT} 18 | noRightParen := SyntaxError{[]string{token.RPAREN}, token.SEMICOLON} 19 | noColumnName := SyntaxError{[]string{token.RPAREN}, token.TEXT} 20 | noColumnType := SyntaxError{[]string{token.TEXT, token.INT}, token.COMMA} 21 | noSemicolon := SyntaxError{[]string{token.SEMICOLON}, ""} 22 | 23 | tests := []errorHandlingTestSuite{ 24 | {"CREATE tbl(one TEXT);", noTableKeyword.Error()}, 25 | {"CREATE TABLE (one TEXT);", noTableName.Error()}, 26 | {"CREATE TABLE tbl one TEXT);", noLeftParen.Error()}, 27 | {"CREATE TABLE tbl (one TEXT;", noRightParen.Error()}, 28 | {"CREATE TABLE tbl (TEXT, two INT);", noColumnName.Error()}, 29 | {"CREATE TABLE tbl (one , two INT);", noColumnType.Error()}, 30 | {"CREATE TABLE tbl (one TEXT, two INT)", noSemicolon.Error()}, 31 | } 32 | 33 | runParserErrorHandlingSuite(t, tests) 34 | 35 | } 36 | 37 | func TestParseDropCommandErrorHandling(t *testing.T) { 38 | missingTableKeywordError := SyntaxError{expecting: []string{token.TABLE}, got: token.IDENT} 39 | missingDropKeywordError := SyntaxInvalidCommandError{token.TABLE} 40 | missingSemicolonError := &SyntaxError{expecting: []string{token.SEMICOLON}, got: ""} 41 | invalidIdentError := &SyntaxError{expecting: []string{token.IDENT}, got: token.LITERAL} 42 | tests := []errorHandlingTestSuite{ 43 | {input: "DROP table;", expectedError: missingTableKeywordError.Error()}, 44 | {input: "TABLE table;", expectedError: missingDropKeywordError.Error()}, 45 | {input: "DROP TABLE table", expectedError: missingSemicolonError.Error()}, 46 | {input: "DROP TABLE 2;", expectedError: invalidIdentError.Error()}, 47 | } 48 | 49 | runParserErrorHandlingSuite(t, tests) 50 | } 51 | 52 | func TestParseInsertCommandErrorHandling(t *testing.T) { 53 | noIntoKeyword := SyntaxError{[]string{token.INTO}, token.IDENT} 54 | noTableName := SyntaxError{[]string{token.IDENT}, token.VALUES} 55 | noLeftParen := SyntaxError{[]string{token.LPAREN}, token.APOSTROPHE} 56 | noValue := SyntaxError{[]string{token.IDENT, token.LITERAL, token.NULL}, token.APOSTROPHE} 57 | noRightParen := SyntaxError{[]string{token.RPAREN}, token.SEMICOLON} 58 | noSemicolon := SyntaxError{[]string{token.SEMICOLON}, ""} 59 | noLeftApostrophe := NoApostropheOnLeftParserError{ident: "hello"} 60 | noRightApostrophe := NoApostropheOnRightParserError{ident: "hello, 10)"} 61 | 62 | tests := []errorHandlingTestSuite{ 63 | {"INSERT tbl VALUES( 'hello', 10);", noIntoKeyword.Error()}, 64 | {"INSERT INTO VALUES( 'hello', 10);", noTableName.Error()}, 65 | {"INSERT INTO tl VALUES 'hello', 10);", noLeftParen.Error()}, 66 | {"INSERT INTO tl VALUES ('', 10);", noValue.Error()}, 67 | {"INSERT INTO tl VALUES ('hello', 10;", noRightParen.Error()}, 68 | {"INSERT INTO tl VALUES ('hello', 10)", noSemicolon.Error()}, 69 | {"INSERT INTO tl VALUES (hello', 10)", noLeftApostrophe.Error()}, 70 | {"INSERT INTO tl VALUES ('hello, 10)", noRightApostrophe.Error()}, 71 | } 72 | 73 | runParserErrorHandlingSuite(t, tests) 74 | 75 | } 76 | 77 | func TestParseUpdateCommandErrorHandling(t *testing.T) { 78 | notableName := SyntaxError{expecting: []string{token.IDENT}, got: token.SEMICOLON} 79 | noSetKeyword := SyntaxError{expecting: []string{token.SET}, got: token.SEMICOLON} 80 | noColumnName := SyntaxError{expecting: []string{token.IDENT}, got: token.LITERAL} 81 | noToKeyword := SyntaxError{expecting: []string{token.TO}, got: token.SEMICOLON} 82 | noSecondIdentOrLiteralForValue := SyntaxError{expecting: []string{token.IDENT, token.LITERAL, token.NULL}, got: token.SEMICOLON} 83 | noCommaBetweenValues := SyntaxError{expecting: []string{token.SEMICOLON, token.WHERE}, got: token.IDENT} 84 | noWhereOrSemicolon := SyntaxError{expecting: []string{token.SEMICOLON, token.WHERE}, got: token.SELECT} 85 | noLeftApostrophe := NoApostropheOnLeftParserError{ident: "new_value_1"} 86 | noRightApostrophe := NoApostropheOnRightParserError{ident: "new_value_1"} 87 | 88 | tests := []errorHandlingTestSuite{ 89 | {"UPDATE;", notableName.Error()}, 90 | {"UPDATE table;", noSetKeyword.Error()}, 91 | {"UPDATE table SET 2;", noColumnName.Error()}, 92 | {"UPDATE table SET column_name_1;", noToKeyword.Error()}, 93 | {"UPDATE table SET column_name_1 TO;", noSecondIdentOrLiteralForValue.Error()}, 94 | {"UPDATE table SET column_name_1 TO 2 column_name_1 TO 3;", noCommaBetweenValues.Error()}, 95 | {"UPDATE table SET column_name_1 TO 'new_value_1' SELECT;", noWhereOrSemicolon.Error()}, 96 | {"UPDATE table SET column_name_1 TO new_value_1'", noLeftApostrophe.Error()}, 97 | {"UPDATE table SET column_name_1 TO 'new_value_1", noRightApostrophe.Error()}, 98 | } 99 | 100 | runParserErrorHandlingSuite(t, tests) 101 | 102 | } 103 | 104 | func TestParseSelectCommandErrorHandling(t *testing.T) { 105 | noFromKeyword := SyntaxError{[]string{token.FROM}, token.IDENT} 106 | noColumns := SyntaxError{[]string{token.ASTERISK, token.IDENT, token.MAX, token.MIN, token.SUM, token.AVG, token.COUNT}, token.FROM} 107 | noTableName := SyntaxError{[]string{token.IDENT}, token.SEMICOLON} 108 | noSemicolon := SyntaxError{[]string{token.SEMICOLON, token.WHERE, token.ORDER, token.LIMIT, token.OFFSET, token.JOIN, token.LEFT, token.RIGHT, token.INNER, token.FULL}, ""} 109 | noAggregateFunctionParenClosure := SyntaxError{[]string{token.RPAREN}, ","} 110 | noAggregateFunctionLeftParen := SyntaxError{[]string{token.LPAREN}, token.IDENT} 111 | noFromAfterAsterisk := SyntaxError{[]string{token.FROM}, ","} 112 | noAsteriskInsideMaxArgument := SyntaxError{[]string{token.IDENT}, "*"} 113 | 114 | tests := []errorHandlingTestSuite{ 115 | {"SELECT column1, column2 tbl;", noFromKeyword.Error()}, 116 | {"SELECT FROM table;", noColumns.Error()}, 117 | {"SELECT column1, column2 FROM ;", noTableName.Error()}, 118 | {"SELECT column1, column2 FROM table", noSemicolon.Error()}, 119 | {"SELECT SUM(column1, column2 FROM table", noAggregateFunctionParenClosure.Error()}, 120 | {"SELECT SUM column1 FROM table", noAggregateFunctionLeftParen.Error()}, 121 | {"SELECT *, colName FROM table", noFromAfterAsterisk.Error()}, 122 | {"SELECT MAX(*) FROM table", noAsteriskInsideMaxArgument.Error()}, 123 | } 124 | 125 | runParserErrorHandlingSuite(t, tests) 126 | } 127 | 128 | func TestParseWhereCommandErrorHandling(t *testing.T) { 129 | selectCommandPrefix := "SELECT * FROM tbl " 130 | noPredecessorError := NoPredecessorParserError{command: token.WHERE} 131 | noColName := LogicalExpressionParsingError{} 132 | noLeftAphostrophe := LogicalExpressionParsingError{} 133 | noOperatorInsideWhereStatementException := LogicalExpressionParsingError{} 134 | valueIsMissing := SyntaxError{expecting: []string{token.APOSTROPHE, token.IDENT, token.LITERAL, token.NULL}, got: token.SEMICOLON} 135 | tokenAnd := token.AND 136 | conjunctionIsMissing := SyntaxError{expecting: []string{token.SEMICOLON, token.ORDER}, got: token.IDENT} 137 | nextLogicalExpressionIsMissing := LogicalExpressionParsingError{afterToken: &tokenAnd} 138 | noSemicolon := SyntaxError{expecting: []string{token.SEMICOLON, token.ORDER}, got: ""} 139 | noLeftParGotSemicolon := SyntaxError{expecting: []string{token.LPAREN}, got: ";"} 140 | noLeftParGotNumber := SyntaxError{expecting: []string{token.LPAREN}, got: token.LITERAL} 141 | noComma := SyntaxError{expecting: []string{token.COMMA, token.RPAREN}, got: token.LITERAL} 142 | anonymitifierInContains := SyntaxError{expecting: []string{token.IDENT}, got: "'one'"} 143 | noInKeywordException := LogicalExpressionParsingError{} 144 | noLeftApostropheGoodbye := NoApostropheOnLeftParserError{ident: "goodbye"} 145 | noLeftApostropheFive := NoApostropheOnLeftParserError{ident: "5"} 146 | noRightApostropheGoodbye := NoApostropheOnRightParserError{ident: "goodbye"} 147 | noRightApostropheGoodbyeBigger := NoApostropheOnRightParserError{ident: "goodbye EQUAL two"} 148 | noRightApostropheFive := NoApostropheOnRightParserError{ident: "5"} 149 | 150 | tests := []errorHandlingTestSuite{ 151 | {"WHERE col1 NOT 'goodbye' OR col2 EQUAL 3;", noPredecessorError.Error()}, 152 | {selectCommandPrefix + "WHERE NOT 'goodbye' OR column2 EQUAL 3;", noColName.Error()}, 153 | {selectCommandPrefix + "WHERE one 'goodbye';", noOperatorInsideWhereStatementException.Error()}, 154 | {selectCommandPrefix + "WHERE one EQUAL;", valueIsMissing.Error()}, 155 | {selectCommandPrefix + "WHERE one EQUAL 5 two NOT 1;", conjunctionIsMissing.Error()}, 156 | {selectCommandPrefix + "WHERE one EQUAL 5 AND;", nextLogicalExpressionIsMissing.Error()}, 157 | {selectCommandPrefix + "WHERE one EQUAL 5 AND two NOT 5", noSemicolon.Error()}, 158 | {selectCommandPrefix + "WHERE one IN ;", noLeftParGotSemicolon.Error()}, 159 | {selectCommandPrefix + "WHERE one IN 5;", noLeftParGotNumber.Error()}, 160 | {selectCommandPrefix + "WHERE one IN (5 6);", noComma.Error()}, 161 | {selectCommandPrefix + "WHERE one IN ('5", noRightApostropheFive.Error()}, 162 | {selectCommandPrefix + "WHERE one IN (5');", noLeftApostropheFive.Error()}, 163 | {selectCommandPrefix + "WHERE 'one' IN (5);", anonymitifierInContains.Error()}, 164 | {selectCommandPrefix + "WHERE one (5, 6);", noInKeywordException.Error()}, 165 | {selectCommandPrefix + "WHERE one EQUAL goodbye';", noLeftApostropheGoodbye.Error()}, 166 | {selectCommandPrefix + "WHERE one EQUAL 'goodbye", noRightApostropheGoodbye.Error()}, 167 | {selectCommandPrefix + "WHERE 'goodbye EQUAL two", noRightApostropheGoodbyeBigger.Error()}, 168 | {selectCommandPrefix + "WHERE goodbye' EQUAL two", noLeftAphostrophe.Error()}, 169 | } 170 | 171 | runParserErrorHandlingSuite(t, tests) 172 | } 173 | 174 | func TestParseOrderByCommandErrorHandling(t *testing.T) { 175 | selectCommandPrefix := "SELECT * FROM tbl " 176 | noPredecessorError := NoPredecessorParserError{command: token.ORDER} 177 | noAscDescError := SyntaxError{expecting: []string{token.ASC, token.DESC}, got: token.SEMICOLON} 178 | noByKeywordError := SyntaxError{expecting: []string{token.BY}, got: token.IDENT} 179 | noIdentKeywordError := SyntaxError{expecting: []string{token.IDENT}, got: token.ASC} 180 | 181 | tests := []errorHandlingTestSuite{ 182 | {"ORDER BY column1;", noPredecessorError.Error()}, 183 | {selectCommandPrefix + "ORDER BY column1;", noAscDescError.Error()}, 184 | {selectCommandPrefix + "ORDER column1 ASC;", noByKeywordError.Error()}, 185 | {selectCommandPrefix + "ORDER BY ASC;", noIdentKeywordError.Error()}, 186 | } 187 | 188 | runParserErrorHandlingSuite(t, tests) 189 | } 190 | 191 | func TestParseLimitCommandErrorHandling(t *testing.T) { 192 | selectCommandPrefix := "SELECT * FROM tbl " 193 | noPredecessorError := NoPredecessorParserError{command: token.LIMIT} 194 | noLiteralError := SyntaxError{expecting: []string{token.LITERAL}, got: token.SEMICOLON} 195 | lessThanZeroError := ArithmeticLessThanZeroParserError{variable: "limit"} 196 | 197 | tests := []errorHandlingTestSuite{ 198 | {"LIMIT 5;", noPredecessorError.Error()}, 199 | {selectCommandPrefix + "LIMIT;", noLiteralError.Error()}, 200 | {selectCommandPrefix + "LIMIT -10;", lessThanZeroError.Error()}, 201 | } 202 | 203 | runParserErrorHandlingSuite(t, tests) 204 | } 205 | 206 | func TestParseOffsetCommandErrorHandling(t *testing.T) { 207 | selectCommandPrefix := "SELECT * FROM tbl " 208 | noPredecessorError := NoPredecessorParserError{command: token.OFFSET} 209 | noLiteralError := SyntaxError{expecting: []string{token.LITERAL}, got: token.IDENT} 210 | lessThanZeroError := ArithmeticLessThanZeroParserError{variable: "offset"} 211 | 212 | tests := []errorHandlingTestSuite{ 213 | {"OFFSET 5;", noPredecessorError.Error()}, 214 | {selectCommandPrefix + "OFFSET hi;", noLiteralError.Error()}, 215 | {selectCommandPrefix + "OFFSET -10;", lessThanZeroError.Error()}, 216 | } 217 | 218 | runParserErrorHandlingSuite(t, tests) 219 | } 220 | 221 | func TestParseDeleteCommandErrorHandling(t *testing.T) { 222 | noFromKeyword := SyntaxError{[]string{token.FROM}, token.IDENT} 223 | noTableName := SyntaxError{[]string{token.IDENT}, token.WHERE} 224 | noWhereCommand := SyntaxError{[]string{token.WHERE}, ";"} 225 | 226 | tests := []errorHandlingTestSuite{ 227 | {"DELETE table WHERE TRUE", noFromKeyword.Error()}, 228 | {"DELETE FROM WHERE TRUE;", noTableName.Error()}, 229 | {"DELETE FROM table;", noWhereCommand.Error()}, 230 | } 231 | 232 | runParserErrorHandlingSuite(t, tests) 233 | } 234 | 235 | func TestPeriodInIdentWhileCreatingTableErrorHandling(t *testing.T) { 236 | illegalPeriodInTableName := IllegalPeriodInIdentParserError{"tab.le"} 237 | illegalPeriodInColumnName := IllegalPeriodInIdentParserError{"col.umn"} 238 | 239 | tests := []errorHandlingTestSuite{ 240 | {"CREATE TABLE tab.le( one TEXT , two INT);", illegalPeriodInTableName.Error()}, 241 | {"CREATE TABLE table1( col.umn TEXT , two INT);", illegalPeriodInColumnName.Error()}, 242 | } 243 | 244 | runParserErrorHandlingSuite(t, tests) 245 | } 246 | 247 | func runParserErrorHandlingSuite(t *testing.T, suite []errorHandlingTestSuite) { 248 | for i, test := range suite { 249 | errorMsg := getErrorMessage(t, test.input, i) 250 | 251 | if errorMsg != test.expectedError { 252 | t.Fatalf("[%v]Was expecting error: \n\t{%s},\n\tbut it was:\n\t{%s}", i, test.expectedError, errorMsg) 253 | } 254 | } 255 | } 256 | 257 | func getErrorMessage(t *testing.T, input string, testIndex int) string { 258 | lexerInstance := lexer.RunLexer(input) 259 | parserInstance := New(lexerInstance) 260 | _, err := parserInstance.ParseSequence() 261 | 262 | if err == nil { 263 | t.Fatalf("[%v]Was expecting error from parser but there was none", testIndex) 264 | } 265 | 266 | return err.Error() 267 | } 268 | -------------------------------------------------------------------------------- /token/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package token contains keywords needed to map input 3 | */ 4 | package token 5 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | type Type string 4 | 5 | // Token - contains 6 | type Token struct { 7 | Type Type 8 | Literal string 9 | } 10 | 11 | const ( 12 | // Operators 13 | ASTERISK = "*" 14 | 15 | // Identifiers & Literals 16 | IDENT = "IDENT" // e.g., table, column names 17 | LITERAL = "LITERAL" // e.g., numeric or string literals 18 | 19 | // Delimiters 20 | COMMA = "," 21 | SEMICOLON = ";" 22 | APOSTROPHE = "'" 23 | 24 | // Parentheses 25 | LPAREN = "(" 26 | RPAREN = ")" 27 | 28 | // Special Tokens 29 | EOF = "" 30 | ILLEGAL = "ILLEGAL" 31 | 32 | // Commands 33 | CREATE = "CREATE" 34 | DROP = "DROP" 35 | TABLE = "TABLE" 36 | INSERT = "INSERT" 37 | INTO = "INTO" 38 | VALUES = "VALUES" 39 | SELECT = "SELECT" 40 | DELETE = "DELETE" 41 | UPDATE = "UPDATE" 42 | 43 | // Clauses 44 | FROM = "FROM" 45 | WHERE = "WHERE" 46 | ORDER = "ORDER" 47 | BY = "BY" 48 | ASC = "ASC" 49 | DESC = "DESC" 50 | LIMIT = "LIMIT" 51 | OFFSET = "OFFSET" 52 | SET = "SET" 53 | DISTINCT = "DISTINCT" 54 | TO = "TO" 55 | 56 | // Joins 57 | JOIN = "JOIN" 58 | INNER = "INNER" 59 | FULL = "FULL" 60 | LEFT = "LEFT" 61 | RIGHT = "RIGHT" 62 | ON = "ON" 63 | 64 | // Aggregates 65 | MIN = "MIN" 66 | MAX = "MAX" 67 | COUNT = "COUNT" 68 | SUM = "SUM" 69 | AVG = "AVG" 70 | 71 | // Logical 72 | EQUAL = "EQUAL" 73 | NOT = "NOT" 74 | AND = "AND" 75 | OR = "OR" 76 | TRUE = "TRUE" 77 | FALSE = "FALSE" 78 | IN = "IN" 79 | NOTIN = "NOTIN" 80 | NULL = "NULL" 81 | 82 | // Data Types 83 | TEXT = "TEXT" 84 | INT = "INT" 85 | ) 86 | 87 | var keywords = map[string]Type{ 88 | "TEXT": TEXT, 89 | "INT": INT, 90 | "CREATE": CREATE, 91 | "DROP": DROP, 92 | "TABLE": TABLE, 93 | "INSERT": INSERT, 94 | "INTO": INTO, 95 | "SELECT": SELECT, 96 | "FROM": FROM, 97 | "DELETE": DELETE, 98 | "ORDER": ORDER, 99 | "BY": BY, 100 | "ASC": ASC, 101 | "DESC": DESC, 102 | "LIMIT": LIMIT, 103 | "OFFSET": OFFSET, 104 | "UPDATE": UPDATE, 105 | "SET": SET, 106 | "DISTINCT": DISTINCT, 107 | "INNER": INNER, 108 | "FULL": FULL, 109 | "LEFT": LEFT, 110 | "RIGHT": RIGHT, 111 | "JOIN": JOIN, 112 | "ON": ON, 113 | "MIN": MIN, 114 | "MAX": MAX, 115 | "COUNT": COUNT, 116 | "SUM": SUM, 117 | "AVG": AVG, 118 | "IN": IN, 119 | "NOTIN": NOTIN, 120 | "TO": TO, 121 | "VALUES": VALUES, 122 | "WHERE": WHERE, 123 | "EQUAL": EQUAL, 124 | "NOT": NOT, 125 | "AND": AND, 126 | "OR": OR, 127 | "TRUE": TRUE, 128 | "FALSE": FALSE, 129 | "NULL": NULL, 130 | } 131 | 132 | // LookupIdent - Return keyword type from defined list if exists, otherwise it returns IDENT type 133 | func LookupIdent(ident string) Type { 134 | if tok, ok := keywords[ident]; ok { 135 | return tok 136 | } 137 | return IDENT 138 | } 139 | --------------------------------------------------------------------------------