├── .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 |
3 |
4 |
5 | # GO4SQL
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------