├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── tag.yml ├── .gitignore ├── .gitmodules ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── README.md ├── autocomplete.go ├── autocomplete_test.go ├── go.mod ├── go.sum ├── howtouse.gif ├── lexical ├── lexemes.go ├── lexical.go ├── lexical_test.go ├── states.go └── tokens.go ├── main.go ├── parser ├── ast.go ├── parser.go └── parser_test.go ├── runtime ├── commits.go ├── commits_test.go ├── reference.go ├── remotes.go ├── runtime.go ├── runtime_test.go ├── visitor.go └── visitor_test.go ├── semantical ├── semantical.go ├── semantical_test.go └── visitor.go ├── tables.md ├── test ├── options.bats ├── select.bats └── use.bats ├── utilities ├── utilities.go └── utilities_test.go └── version.txt /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | # Github Actions updates 5 | - package-ecosystem: github-actions 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | commit-message: 10 | prefix: 'ci:' 11 | # Labels on pull requests 12 | labels: 13 | - 'GitHub dependencies' 14 | 15 | # Go Packages updates 16 | - package-ecosystem: gomod 17 | directory: / 18 | schedule: 19 | interval: weekly 20 | commit-message: 21 | prefix: 'deps:' 22 | # Labels on pull requests 23 | labels: 24 | - 'Go dependencies' 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | go: [ '1.20' ] 15 | os: [ 'ubuntu-22.04', 'macos-12', 'windows-2022' ] 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Go build 23 | run: | 24 | git rev-parse --short "$GITHUB_SHA" | tr -d '\n' > ./version.txt 25 | go build . 26 | ./gitql -v 27 | 28 | unit: 29 | needs: 30 | - build 31 | runs-on: ${{ matrix.os }} 32 | strategy: 33 | matrix: 34 | go: [ '1.20' ] # Check the binary of the latest go version is enough 35 | os: [ 'ubuntu-22.04' ] 36 | steps: 37 | - name: Set up Go ${{ matrix.go }} 38 | uses: actions/setup-go@v5 39 | with: 40 | go-version: ${{ matrix.go }} 41 | 42 | - name: Check out code 43 | uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 46 | 47 | - name: Run unit tests 48 | run: go test -v ./... 49 | 50 | functional: 51 | needs: 52 | - unit 53 | runs-on: ${{ matrix.os }} 54 | strategy: 55 | matrix: 56 | go: [ '1.20' ] # Check the binary of the latest go version is enough 57 | os: [ 'ubuntu-22.04', 'macos-12', 'windows-2022' ] 58 | steps: 59 | - name: Check out code 60 | uses: actions/checkout@v4 61 | with: 62 | submodules: true 63 | fetch-depth: 0 64 | 65 | - name: Setup BATS 66 | uses: mig4/setup-bats@v1 67 | with: 68 | bats-version: 1.2.1 69 | 70 | - name: Run functional tests 71 | run: | 72 | git stash -u 73 | go build . 74 | ./gitql -v 75 | bats ./test 76 | 77 | upload: 78 | needs: 79 | - functional 80 | runs-on: ${{ matrix.os }} 81 | strategy: 82 | matrix: 83 | go: [ '1.20' ] # Check the binary of the latest go version is enough 84 | os: [ 'ubuntu-22.04', 'macos-12', 'windows-2022' ] 85 | steps: 86 | - uses: actions/checkout@v4 87 | - run: go build . 88 | - uses: actions/upload-artifact@master 89 | name: Upload binary ${{ runner.os }} 90 | with: 91 | name: gitql-${{ runner.os }} 92 | path: ./gitql* 93 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - 17 | name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.18 21 | - 22 | name: Set up version.txt 23 | run: | 24 | git update-index --assume-unchanged version.txt 25 | git describe --tags --abbrev=0 | tr -d '\n' > ./version.txt 26 | echo "version found:" 27 | cat ./version.txt 28 | - 29 | name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v6 31 | with: 32 | distribution: goreleaser 33 | version: latest 34 | args: release --rm-dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-* 2 | git2go 3 | libgit2 4 | gitql 5 | .DS_Store 6 | .vscode* 7 | .idea/ 8 | vendor 9 | test/test_helper/ 10 | test/bats/ 11 | .version.txt -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/bats"] 2 | path = test/bats 3 | url = https://github.com/bats-core/bats-core.git 4 | [submodule "test/test_helper/bats-support"] 5 | path = test/test_helper/bats-support 6 | url = https://github.com/bats-core/bats-support.git 7 | [submodule "test/test_helper/bats-assert"] 8 | path = test/test_helper/bats-assert 9 | url = https://github.com/bats-core/bats-assert.git 10 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | - go generate ./... 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - windows 11 | - darwin 12 | archives: 13 | - replacements: 14 | darwin: Darwin 15 | linux: Linux 16 | windows: Windows 17 | 386: i386 18 | amd64: x86_64 19 | checksum: 20 | name_template: 'checksums.txt' 21 | snapshot: 22 | name_template: "{{ .Tag }}-next" 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs:' 28 | - '^test:' 29 | release: 30 | github: 31 | disable: false 32 | draft: false 33 | name_template: "{{.Tag}}" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # run with: 2 | # docker build -t gitql . 3 | # docker run -it --entrypoint /bin/sh gitql:latest 4 | 5 | FROM golang:1.20.7-alpine3.18 as builder 6 | 7 | WORKDIR /src 8 | COPY go.mod . 9 | COPY go.sum . 10 | COPY main.go autocomplete.go version.txt ./ 11 | COPY lexical lexical 12 | COPY parser parser 13 | COPY runtime runtime 14 | COPY semantical semantical 15 | COPY utilities utilities 16 | RUN go mod download 17 | RUN CGO_ENABLED=0 GOOS=linux go build -o /bin/gitql 18 | 19 | FROM alpine:3.18 20 | COPY --from=builder /bin/gitql /bin/ 21 | 22 | ENTRYPOINT ["gitql"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Claudson Oliveira 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 | Gitql 2 | ![](https://github.com/cloudson/gitql/workflows/CI/badge.svg) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/filhodanuvem/gitql)](https://goreportcard.com/report/github.com/filhodanuvem/gitql) 4 | [![Open Source Helpers](https://www.codetriage.com/filhodanuvem/gitql/badges/users.svg)](https://www.codetriage.com/filhodanuvem/gitql) 5 | 6 | License MIT 7 | 8 | =============== 9 | 10 | Gitql is a Git query language. 11 | 12 | In a repository path... 13 | 14 | ![how to use](howtouse.gif) 15 | 16 | See more [here](https://asciinema.org/a/97094) 17 | 18 | ## Reading the code 19 | ⚠️ Gitql is my first golang project. If you are a beginner looking for using the project as a guideline (how to organise or make an idiomatic go code), I recommend you [polyglot](https://github.com/filhodanuvem/polyglot) instead. 20 | 21 | ## Requirements 22 | - Go 1.16+ 23 | 24 | ## How to install 25 | 26 | You can access the [releases page](https://github.com/cloudson/gitql/releases) and just grab the binary. If you want to compile itself just run `go build .`. 27 | 28 | ## Examples 29 | 30 | `gitql "your query" ` 31 | or 32 | `git ql "your query" ` 33 | 34 | As an example, this is the `commits` table: 35 | 36 | | commits | 37 | | ---------| 38 | | author | 39 | | author_email | 40 | | committer | 41 | | committer_email | 42 | | hash | 43 | | date | 44 | | message | 45 | | full_message | 46 | 47 | (see more tables [here](tables.md)) 48 | 49 | ## Example Commands 50 | * `select hash, author, message from commits limit 3` 51 | * `select hash, message from commits where 'hell' in full_message or 'Fuck' in full_message` 52 | * `select hash, message, author_email from commits where author = 'cloudson'` 53 | * `select date, message from commits where date < '2014-04-10'` 54 | * `select message from commits where 'hell' in message order by date asc` 55 | * `select distinct author from commits where date < '2020-01-01'` 56 | 57 | ## Questions? 58 | 59 | `gitql` or open an [issue](https://github.com/cloudson/gitql/issues) 60 | 61 | Notes: 62 | * Gitql doesn't want to _kill_ `git log` - it was created just for science! :sweat_smile: 63 | * It's read-only - no deleting, inserting, or updating tables or commits. :stuck_out_tongue_closed_eyes: 64 | * The default limit is 10 rows. 65 | * It's inspired by [textql](https://github.com/dinedal/textql). 66 | * Gitql is a compiler/interpreter instead of just read a sqlite database with all commits, tags, etc. because we would need to sync the tables every time before run sql and we would have sqlite bases for each repository. :neutral_face: 67 | -------------------------------------------------------------------------------- /autocomplete.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func suggestColumnsFromLatest(focused string) [][]rune { 4 | return suggestLatest(focused[:len(focused)-1], [][]string{ 5 | []string{"hash", "date", "author", "author_email", "committer", "committer_email", "message", "full_message"}, 6 | []string{"name", "full_name", "type", "hash"}, 7 | []string{"name", "url", "push_url", "owner"}, 8 | []string{"name", "full_name", "hash"}, 9 | }) 10 | } 11 | 12 | // Creates a candidate from the input previous character string. 13 | func suggestLatest(focused string, candidacies [][]string) [][]rune { 14 | var suggests [][]rune 15 | for _, candidacy := range candidacies { 16 | s := getPartsFromSlice(focused, candidacy) 17 | if s != nil { 18 | suggests = append(suggests, s...) 19 | } 20 | } 21 | removeDuplicates(&suggests) 22 | 23 | return suggests 24 | } 25 | 26 | func containColumns(focused string) bool { 27 | _, ok := isContained(focused, []string{ 28 | "select", // gitql> select [tab 29 | "distinct,", 30 | "name,", 31 | "url,", 32 | "push_url,", 33 | "owner,", 34 | "full_name,", 35 | "hash,", 36 | "date,", 37 | "author,", 38 | "author_email,", 39 | "committer,", 40 | "committer_email,", 41 | "message,", 42 | "full_message,", 43 | "type,", 44 | }) 45 | return ok 46 | } 47 | 48 | // Remove duplicates elements in slice. 49 | func removeDuplicates(s *[][]rune) { 50 | found := make(map[string]bool) 51 | j := 0 52 | for i, x := range *s { 53 | key := string(x) 54 | if !found[key] { 55 | found[key] = true 56 | (*s)[j] = (*s)[i] 57 | j++ 58 | } 59 | } 60 | *s = (*s)[:j] 61 | } 62 | 63 | func suggestQuery(inputs [][]rune, pos int) [][]rune { 64 | 65 | ln := len(inputs) 66 | 67 | if ln == 1 { 68 | // When nothing is input yet 69 | return [][]rune{[]rune("select")} 70 | } 71 | focused := string(inputs[ln-2]) 72 | if focused == "select" { 73 | // gitql> select [tab 74 | // In the case where the most recent input is "select" 75 | return [][]rune{ 76 | []rune("distinct"), 77 | []rune("*"), 78 | []rune("name"), 79 | []rune("url"), 80 | []rune("push_url"), 81 | []rune("owner"), 82 | []rune("full_name"), 83 | []rune("hash"), 84 | []rune("date"), 85 | []rune("author"), 86 | []rune("author_email"), 87 | []rune("committer"), 88 | []rune("committer_email"), 89 | []rune("message"), 90 | []rune("full_message"), 91 | []rune("type"), 92 | } 93 | } else if containColumns(focused) { 94 | // gitql> select name, [tab 95 | // gitql> select committer, [tab 96 | // In the case where the most recent input is the column name and comma 97 | return suggestColumnsFromLatest(focused) 98 | } else if focused == "from" { 99 | // gitql> select * from [tab 100 | // In the case after inputted "from" 101 | return [][]rune{ 102 | []rune("tags"), 103 | []rune("branches"), 104 | []rune("commits"), 105 | []rune("refs"), 106 | } 107 | } else if focused == "order" { 108 | return [][]rune{[]rune("by")} 109 | } else if focused == "where" || focused == "by" || focused == "or" || focused == "and" { 110 | // gitql> select name from commits where [tab 111 | // gitql> select * from commits where committer = "K" order by [tab 112 | // gitql> select * from commits where committer = "K" and [tab 113 | // In the case is inputted after "where", "by", "and", "or" 114 | var table string 115 | for i := 0; i < len(inputs); i++ { 116 | if string(inputs[i]) == "from" { 117 | i++ 118 | table = string(inputs[i]) 119 | } 120 | } 121 | 122 | switch table { 123 | case "commits": 124 | return [][]rune{ 125 | []rune("hash"), 126 | []rune("date"), 127 | []rune("author"), 128 | []rune("author_email"), 129 | []rune("committer"), 130 | []rune("committer_email"), 131 | []rune("message"), 132 | []rune("full_message"), 133 | } 134 | case "refs": 135 | return [][]rune{ 136 | []rune("name"), 137 | []rune("full_name"), 138 | []rune("type"), 139 | []rune("hash"), 140 | } 141 | case "branches", "tags": 142 | return [][]rune{ 143 | []rune("name"), 144 | []rune("full_name"), 145 | []rune("hash"), 146 | } 147 | } 148 | } 149 | 150 | return [][]rune{ 151 | []rune("select"), 152 | []rune("from"), 153 | []rune("where"), 154 | []rune("order"), 155 | []rune("by"), 156 | []rune("or"), 157 | []rune("and"), 158 | []rune("limit"), 159 | []rune("in"), 160 | []rune("asc"), 161 | []rune("desc"), 162 | } 163 | } 164 | 165 | func getPartsFromSlice(focused string, candidacy []string) [][]rune { 166 | idx, ok := isContained(focused, candidacy) 167 | if ok { 168 | var suggests [][]rune 169 | for i, v := range candidacy { 170 | // Create slices other than what was focused 171 | if i != idx { 172 | suggests = append(suggests, []rune(v)) 173 | } 174 | } 175 | return suggests 176 | } 177 | return nil 178 | } 179 | 180 | func isContained(focused string, candidacy []string) (int, bool) { 181 | for idx, val := range candidacy { 182 | if focused == val { 183 | return idx, true 184 | } 185 | } 186 | return -1, false 187 | } 188 | -------------------------------------------------------------------------------- /autocomplete_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestSuggestColumnsFromLatest(t *testing.T) { 9 | answer := []string{ 10 | "hash", 11 | "type", 12 | "date", 13 | "author", 14 | "author_email", 15 | "committer", 16 | "committer_email", 17 | "message", 18 | "full_message", 19 | "name", 20 | "full_name", 21 | } 22 | 23 | expected := createHashMap(answer) 24 | result := suggestColumnsFromLatest("hash,") 25 | for _, v := range result { 26 | if _, ok := expected[string(v)]; !ok { 27 | t.Errorf("expected 'hash', 'type', 'date', 'author', 'author_email', 'committer', 'committer_email', 'message', 'full_message', 'name', 'full_name' got %s", string(v)) 28 | } 29 | } 30 | } 31 | 32 | func TestRemoveDuplicates(t *testing.T) { 33 | words := [][]rune{ 34 | []rune("alpaca"), 35 | []rune("alpaca"), 36 | []rune("Code-Hex"), 37 | []rune("Hello"), 38 | []rune("Hello"), 39 | []rune("World"), 40 | } 41 | 42 | removeDuplicates(&words) 43 | 44 | if len(words) != 4 { 45 | t.Error("Failed to remove duplicates") 46 | } 47 | 48 | if string(words[0]) != "alpaca" { 49 | t.Errorf("expected alpaca, got %s", string(words[0])) 50 | } 51 | 52 | if string(words[1]) != "Code-Hex" { 53 | t.Errorf("expected Code-Hex, got %s", string(words[1])) 54 | } 55 | 56 | if string(words[2]) != "Hello" { 57 | t.Errorf("expected Hello, got %s", string(words[2])) 58 | } 59 | 60 | if string(words[3]) != "World" { 61 | t.Errorf("expected World, got %s", string(words[3])) 62 | } 63 | } 64 | 65 | func TestIsContained(t *testing.T) { 66 | idx, ok := isContained("Code-Hex", []string{"cloudson", "luizperes", "Code-Hex"}) 67 | if !ok { 68 | t.Error("Failed to invoke `isContained`") 69 | } 70 | 71 | if idx != 2 { 72 | t.Errorf("expected %d, got %d", 2, idx) 73 | } 74 | } 75 | 76 | func TestGetPartsFromSlice(t *testing.T) { 77 | gotSlice := getPartsFromSlice("luizperes", []string{"cloudson", "luizperes", "Code-Hex"}) 78 | if len(gotSlice) != 2 { 79 | t.Error("Failed to invoke `getPartsFromSlice`") 80 | } 81 | 82 | if string(gotSlice[0]) != "cloudson" { 83 | t.Errorf("expected cloudson, got %s", string(gotSlice[0])) 84 | } 85 | 86 | if string(gotSlice[1]) != "Code-Hex" { 87 | t.Errorf("expected Code-Hex, got %s", string(gotSlice[1])) 88 | } 89 | 90 | notSlice := getPartsFromSlice("gopher", []string{"cloudson", "luizperes", "Code-Hex"}) 91 | if len(notSlice) != 0 { 92 | t.Error("Failed to invoke `getPartsFromSlice`") 93 | } 94 | } 95 | 96 | func TestSuggestQuery(t *testing.T) { 97 | // gitql> [tab 98 | // expected: select 99 | pattern1 := [][]rune{ 100 | []rune(""), 101 | } 102 | assertSuggestsQuery(t, pattern1, []string{"select"}) 103 | 104 | // gitql> select [tab 105 | // expected: *, name, url, push_url, owner, full_name, hash, date, author, 106 | // author_email, committer, committer_email, message, full_message, type 107 | pattern2 := [][]rune{ 108 | []rune("select"), 109 | []rune(""), 110 | } 111 | assertSuggestsQuery(t, pattern2, []string{ 112 | "distinct", 113 | "*", 114 | "name", 115 | "url", 116 | "push_url", 117 | "owner", 118 | "full_name", 119 | "hash", 120 | "date", 121 | "author", 122 | "author_email", 123 | "committer", 124 | "committer_email", 125 | "message", 126 | "full_message", 127 | "type", 128 | }) 129 | 130 | // gitql> select name [tab 131 | // expected: select, from, where, order, by, or, and, limit, in, asc, desc 132 | pattern3 := [][]rune{ 133 | []rune("select"), 134 | []rune("name"), 135 | []rune(""), 136 | } 137 | assertSuggestsQuery(t, pattern3, []string{ 138 | "select", 139 | "from", 140 | "where", 141 | "order", 142 | "by", 143 | "or", 144 | "and", 145 | "limit", 146 | "in", 147 | "asc", 148 | "desc", 149 | }) 150 | // gitql> select name, [tab 151 | // expected: full_name, type, hash, url, push_url, owner 152 | pattern4 := [][]rune{ 153 | []rune("select"), 154 | []rune("name,"), 155 | []rune(""), 156 | } 157 | assertSuggestsQuery(t, pattern4, []string{ 158 | "full_name", 159 | "type", 160 | "hash", 161 | "url", 162 | "push_url", 163 | "owner", 164 | }) 165 | 166 | // gitql> select * from [tab 167 | // expected: tags, branches, commits, refs 168 | pattern5 := [][]rune{ 169 | []rune("select"), 170 | []rune("*"), 171 | []rune("from"), 172 | []rune(""), 173 | } 174 | assertSuggestsQuery(t, pattern5, []string{ 175 | "tags", 176 | "branches", 177 | "commits", 178 | "refs", 179 | }) 180 | 181 | // gitql> select name from refs where [tab 182 | // expected: name, url, push_url, owner 183 | pattern6 := [][]rune{ 184 | []rune("select"), 185 | []rune("name"), 186 | []rune("from"), 187 | []rune("refs"), 188 | []rune("where"), 189 | []rune(""), 190 | } 191 | assertSuggestsQuery(t, pattern6, []string{"name", "full_name", "type", "hash"}) 192 | 193 | // gitql> select committer from commits where committer = "K" and [tab 194 | // expected: hash, date, author, author_email, committer, committer_email, message, full_message 195 | pattern7 := [][]rune{ 196 | []rune("select"), 197 | []rune("committer"), 198 | []rune("from"), 199 | []rune("commits"), 200 | []rune("where"), 201 | []rune("committer"), 202 | []rune("="), 203 | []rune(`"K"`), 204 | []rune("and"), 205 | []rune(""), 206 | } 207 | assertSuggestsQuery(t, pattern7, []string{ 208 | "hash", 209 | "date", 210 | "author", 211 | "author_email", 212 | "committer", 213 | "committer_email", 214 | "message", 215 | "full_message", 216 | }) 217 | 218 | // gitql> select committer from commits where committer = "k" order [tab 219 | // expected: by 220 | pattern8 := [][]rune{ 221 | []rune("select"), 222 | []rune("committer"), 223 | []rune("from"), 224 | []rune("commits"), 225 | []rune("where"), 226 | []rune("committer"), 227 | []rune("="), 228 | []rune(`"K"`), 229 | []rune("order"), 230 | []rune(""), 231 | } 232 | assertSuggestsQuery(t, pattern8, []string{"by"}) 233 | } 234 | 235 | // tiny tools 236 | func assertSuggestsQuery(t *testing.T, inputs [][]rune, expected []string) { 237 | result := suggestQuery(inputs, len(inputs[len(inputs)-1])) 238 | expectedHash := createHashMap(expected) 239 | 240 | for _, v := range result { 241 | _, ok := expectedHash[string(v)] 242 | if !ok { 243 | t.Errorf("expected: (%s), got: %s", strings.Join(expected, ", "), string(v)) 244 | break 245 | } 246 | } 247 | } 248 | func createHashMap(s []string) map[string]bool { 249 | h := make(map[string]bool, len(s)) 250 | for _, key := range s { 251 | h[key] = true 252 | } 253 | return h 254 | } 255 | 256 | func assertSuggests(t *testing.T, expected string, got string) { 257 | if expected != got { 258 | t.Errorf("expected %s, got %s", expected, got) 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cloudson/gitql 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e 7 | github.com/go-git/go-git/v5 v5.6.1 8 | github.com/olekukonko/tablewriter v0.0.5 9 | github.com/urfave/cli/v2 v2.25.7 10 | ) 11 | 12 | require ( 13 | github.com/Microsoft/go-winio v0.5.2 // indirect 14 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 15 | github.com/acomagu/bufpipe v1.0.4 // indirect 16 | github.com/chzyer/logex v1.1.10 // indirect 17 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect 18 | github.com/cloudflare/circl v1.1.0 // indirect 19 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 20 | github.com/emirpasic/gods v1.18.1 // indirect 21 | github.com/go-git/gcfg v1.5.0 // indirect 22 | github.com/go-git/go-billy/v5 v5.4.1 // indirect 23 | github.com/imdario/mergo v0.3.13 // indirect 24 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 25 | github.com/kevinburke/ssh_config v1.2.0 // indirect 26 | github.com/mattn/go-runewidth v0.0.9 // indirect 27 | github.com/pjbgf/sha1cd v0.3.0 // indirect 28 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 29 | github.com/sergi/go-diff v1.1.0 // indirect 30 | github.com/skeema/knownhosts v1.1.0 // indirect 31 | github.com/xanzy/ssh-agent v0.3.3 // indirect 32 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 33 | golang.org/x/crypto v0.21.0 // indirect 34 | golang.org/x/net v0.23.0 // indirect 35 | golang.org/x/sys v0.18.0 // indirect 36 | gopkg.in/warnings.v0 v0.1.2 // indirect 37 | ) 38 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= 2 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 3 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= 4 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= 5 | github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= 6 | github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= 7 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 8 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 9 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 10 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 11 | github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 12 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 13 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 14 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 15 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 16 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 17 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 18 | github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= 19 | github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= 20 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 21 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 22 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 27 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 28 | github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= 29 | github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= 30 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 31 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 32 | github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 33 | github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= 34 | github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= 35 | github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ= 36 | github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= 37 | github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= 38 | github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= 39 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 40 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 41 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 42 | github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= 43 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 44 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 45 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 46 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 47 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 48 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 49 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 50 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 51 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 52 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 53 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 54 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 55 | github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 56 | github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 57 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 58 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 59 | github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= 60 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 61 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 62 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 63 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 64 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 65 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 66 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 70 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 71 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 72 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 73 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 74 | github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= 75 | github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag= 76 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 77 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 78 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 79 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 80 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= 82 | github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 83 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 84 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 85 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 86 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 87 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 88 | golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 89 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 90 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 91 | golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 92 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 93 | golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 94 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 95 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 96 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 97 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 98 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 99 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 100 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 101 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 102 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 103 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 104 | golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 105 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 106 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 107 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 108 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 109 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 110 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 111 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 112 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 115 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 117 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 118 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 119 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 120 | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 121 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 122 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 123 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 124 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 125 | golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 126 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 130 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 131 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 132 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 133 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 134 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 135 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 136 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 137 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 138 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 139 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 140 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 141 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 142 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 143 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 144 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 145 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 146 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 147 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 148 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 149 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 150 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 151 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 152 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 153 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 154 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 155 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 156 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 157 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 158 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 159 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 160 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 161 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 162 | -------------------------------------------------------------------------------- /howtouse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filhodanuvem/gitql/9aab2f5d0eaf9984720c0036653147b4b4253b88/howtouse.gif -------------------------------------------------------------------------------- /lexical/lexemes.go: -------------------------------------------------------------------------------- 1 | package lexical 2 | 3 | const L_SELECT = "select" 4 | const L_DISTINCT = "distinct" 5 | const L_FROM = "from" 6 | const L_WHERE = "where" 7 | const L_ORDER = "order" 8 | const L_BY = "by" 9 | const L_OR = "or" 10 | const L_AND = "and" 11 | const L_LIMIT = "limit" 12 | const L_IN = "in" 13 | const L_ASC = "asc" 14 | const L_DESC = "desc" 15 | const L_LIKE = "like" 16 | const L_NOT = "not" 17 | const L_COUNT = "count" 18 | const L_SHOW = "show" 19 | const L_TABLES = "tables" 20 | const L_DATABASES = "databases" 21 | const L_USE = "use" 22 | -------------------------------------------------------------------------------- /lexical/lexical.go: -------------------------------------------------------------------------------- 1 | package lexical 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | ) 8 | 9 | var source string 10 | var currentPointer int 11 | var CurrentLexeme string 12 | var Command uint8 13 | 14 | type TokenError struct { 15 | char int32 16 | } 17 | 18 | func (error *TokenError) Error() string { 19 | character := string(error.char) 20 | if char == T_EOF { 21 | character = "EOF" 22 | } 23 | return fmt.Sprintf("Unexpected char '%s' with source '%s'", character, source) 24 | } 25 | 26 | func throwTokenError(char int32) *TokenError { 27 | error := new(TokenError) 28 | error.char = char 29 | 30 | return error 31 | } 32 | 33 | var char int32 34 | 35 | func New(s string) { 36 | source = s 37 | currentPointer = 0 38 | char = nextChar() 39 | var err *TokenError 40 | Command, err = Token() 41 | if err != nil { 42 | Command = T_FUCK 43 | } 44 | currentPointer = 0 45 | char = nextChar() 46 | } 47 | 48 | func Token() (uint8, *TokenError) { 49 | var lexeme string 50 | defer func() { 51 | CurrentLexeme = lexeme 52 | }() 53 | state := S_START 54 | for true { 55 | switch state { 56 | case S_START: 57 | if unicode.IsLetter(char) { 58 | state = S_ID 59 | break 60 | } else if unicode.IsNumber(char) { 61 | state = S_NUMERIC 62 | } else { 63 | lexeme = lexeme + string(char) 64 | switch lexeme { 65 | case "*": 66 | state = S_WILD_CARD 67 | break 68 | case "(": 69 | state = S_PARENTH_L 70 | break 71 | case ")": 72 | state = S_PARENTH_R 73 | break 74 | case ",": 75 | state = S_COMMA 76 | break 77 | case ";": 78 | state = S_SEMICOLON 79 | break 80 | case ">": 81 | state = S_GREATER 82 | break 83 | case "<": 84 | state = S_SMALLER 85 | break 86 | case "=": 87 | state = S_EQUAL 88 | break 89 | case "!": 90 | state = S_NOT_EQUAL 91 | break 92 | case "'": 93 | lexeme = "" 94 | char = nextChar() 95 | state = S_LITERAL 96 | break 97 | case "\"": 98 | lexeme = "" 99 | char = nextChar() 100 | state = S_LITERAL_2 101 | break 102 | case " ": 103 | lexeme = "" 104 | char = nextChar() 105 | state = S_START 106 | break 107 | default: 108 | if char == T_EOF { 109 | return T_EOF, nil 110 | } 111 | return T_FUCK, throwTokenError(char) 112 | } 113 | } 114 | break 115 | case S_ID: 116 | for unicode.IsLetter(char) || unicode.IsNumber(char) || string(char) == "_" || string(char) == "-" { 117 | lexeme = lexeme + string(char) 118 | char = nextChar() 119 | } 120 | return lexemeToToken(lexeme), nil 121 | case S_NUMERIC: 122 | for unicode.IsNumber(char) { 123 | lexeme = lexeme + string(char) 124 | char = nextChar() 125 | } 126 | return T_NUMERIC, nil 127 | case S_WILD_CARD: 128 | char = nextChar() 129 | return T_WILD_CARD, nil 130 | case S_COMMA: 131 | char = nextChar() 132 | return T_COMMA, nil 133 | case S_SEMICOLON: 134 | char = nextChar() 135 | return T_SEMICOLON, nil 136 | case S_GREATER: 137 | char = nextChar() 138 | lexeme = string(char) 139 | if lexeme == "=" { 140 | state = S_GREATER_OR_EQUAL 141 | break 142 | } 143 | return T_GREATER, nil 144 | case S_GREATER_OR_EQUAL: 145 | char = nextChar() 146 | return T_GREATER_OR_EQUAL, nil 147 | case S_SMALLER: 148 | char = nextChar() 149 | lexeme = string(char) 150 | if lexeme == "=" { 151 | state = S_SMALLER_OR_EQUAL 152 | break 153 | } else if lexeme == ">" { 154 | char = nextChar() 155 | return T_NOT_EQUAL, nil 156 | } 157 | return T_SMALLER, nil 158 | case S_SMALLER_OR_EQUAL: 159 | char = nextChar() 160 | return T_SMALLER_OR_EQUAL, nil 161 | case S_EQUAL: 162 | char = nextChar() 163 | return T_EQUAL, nil 164 | case S_NOT_EQUAL: 165 | char = nextChar() 166 | lexeme = string(char) 167 | if lexeme == "=" { 168 | char = nextChar() 169 | return T_NOT_EQUAL, nil 170 | } 171 | return 0, throwTokenError(char) 172 | case S_LITERAL: 173 | for string(char) != "'" && char != T_EOF { 174 | lexeme = lexeme + string(char) 175 | char = nextChar() 176 | } 177 | if char == T_EOF { 178 | return 0, throwTokenError(char) 179 | } 180 | char = nextChar() 181 | return T_LITERAL, nil 182 | case S_LITERAL_2: 183 | for string(char) != "\"" && char != T_EOF { 184 | lexeme = lexeme + string(char) 185 | char = nextChar() 186 | } 187 | if char == T_EOF { 188 | return 0, throwTokenError(char) 189 | } 190 | char = nextChar() 191 | return T_LITERAL, nil 192 | case S_PARENTH_L: 193 | char = nextChar() 194 | return T_PARENTH_L, nil 195 | case S_PARENTH_R: 196 | char = nextChar() 197 | return T_PARENTH_R, nil 198 | default: 199 | state = S_START 200 | } 201 | } 202 | return T_EOF, throwTokenError(char) 203 | } 204 | 205 | func lexemeToToken(lexeme string) uint8 { 206 | switch strings.ToLower(lexeme) { 207 | case L_SELECT: 208 | return T_SELECT 209 | case L_DISTINCT: 210 | return T_DISTINCT 211 | case L_FROM: 212 | return T_FROM 213 | case L_WHERE: 214 | return T_WHERE 215 | case L_ORDER: 216 | return T_ORDER 217 | case L_BY: 218 | return T_BY 219 | case L_OR: 220 | return T_OR 221 | case L_AND: 222 | return T_AND 223 | case L_LIMIT: 224 | return T_LIMIT 225 | case L_IN: 226 | return T_IN 227 | case L_ASC: 228 | return T_ASC 229 | case L_DESC: 230 | return T_DESC 231 | case L_LIKE: 232 | return T_LIKE 233 | case L_NOT: 234 | return T_NOT 235 | case L_COUNT: 236 | return T_COUNT 237 | case L_SHOW: 238 | return T_SHOW 239 | case L_TABLES: 240 | return T_TABLES 241 | case L_DATABASES: 242 | return T_DATABASES 243 | case L_USE: 244 | return T_USE 245 | } 246 | return T_ID 247 | } 248 | 249 | func nextChar() int32 { 250 | defer func() { 251 | currentPointer = currentPointer + 1 252 | }() 253 | 254 | if currentPointer >= len(source) { 255 | return T_EOF 256 | } 257 | 258 | return int32(source[currentPointer]) 259 | } 260 | 261 | func rewind() { 262 | currentPointer = 0 263 | } 264 | -------------------------------------------------------------------------------- /lexical/lexical_test.go: -------------------------------------------------------------------------------- 1 | package lexical 2 | 3 | import "testing" 4 | 5 | func setUp() { 6 | rewind() 7 | } 8 | 9 | func TestGetNextChar(t *testing.T) { 10 | setUp() 11 | source = "gopher" 12 | 13 | var expected int32 14 | expected = 'g' 15 | char := nextChar() 16 | assertChar(t, expected, char) 17 | 18 | expected = 'o' 19 | char = nextChar() 20 | assertChar(t, expected, char) 21 | 22 | expected = 'p' 23 | char = nextChar() 24 | assertChar(t, expected, char) 25 | } 26 | 27 | func TestEndOfFile(t *testing.T) { 28 | setUp() 29 | source = "go" 30 | 31 | var expected int32 32 | expected = 'g' 33 | char := nextChar() 34 | assertChar(t, expected, char) 35 | 36 | expected = 'o' 37 | char = nextChar() 38 | assertChar(t, expected, char) 39 | 40 | expected = 0 41 | char = nextChar() 42 | assertChar(t, expected, char) 43 | } 44 | 45 | func TestRecognizeAnToken(t *testing.T) { 46 | setUp() 47 | source = ";" 48 | char = nextChar() 49 | 50 | var token uint8 51 | token, _ = Token() 52 | 53 | assertToken(t, token, T_SEMICOLON) 54 | } 55 | 56 | func TestRecognizeASequenceOfTokens(t *testing.T) { 57 | setUp() 58 | source = "*,>" 59 | char = nextChar() 60 | 61 | var token uint8 62 | 63 | token, _ = Token() 64 | assertToken(t, token, T_WILD_CARD) 65 | 66 | token, _ = Token() 67 | assertToken(t, token, T_COMMA) 68 | 69 | token, _ = Token() 70 | assertToken(t, token, T_GREATER) 71 | } 72 | 73 | func TestRecognizeTokensWithLexemesOfTwoChars(t *testing.T) { 74 | setUp() 75 | source = ">= <=" 76 | char = nextChar() 77 | 78 | var token uint8 79 | 80 | token, _ = Token() 81 | assertToken(t, token, T_GREATER_OR_EQUAL) 82 | 83 | token, _ = Token() 84 | assertToken(t, token, T_SMALLER_OR_EQUAL) 85 | } 86 | 87 | func TestRecognizeTokensWithSourceManySpaced(t *testing.T) { 88 | setUp() 89 | source = "= < >= != cloudson count" 90 | char = nextChar() 91 | 92 | var token uint8 93 | 94 | token, _ = Token() 95 | assertToken(t, token, T_EQUAL) 96 | 97 | token, _ = Token() 98 | assertToken(t, token, T_SMALLER) 99 | 100 | token, _ = Token() 101 | assertToken(t, token, T_GREATER_OR_EQUAL) 102 | 103 | token, _ = Token() 104 | assertToken(t, token, T_NOT_EQUAL) 105 | 106 | token, _ = Token() 107 | assertToken(t, token, T_ID) 108 | 109 | token, _ = Token() 110 | assertToken(t, token, T_COUNT) 111 | } 112 | 113 | func TestErrorUnrecognizeChar(t *testing.T) { 114 | cases := []string{ 115 | "!", "&", "|", 116 | } 117 | 118 | for _, c := range cases { 119 | setUp() 120 | source = c 121 | char = nextChar() 122 | 123 | _, error := Token() 124 | if error == nil { 125 | t.Errorf("Expected error with char '%s' ", c) 126 | } 127 | } 128 | 129 | } 130 | 131 | func TestReservedWords(t *testing.T) { 132 | setUp() 133 | source = "SELECT distinct from WHEre in not cOuNt" 134 | char = nextChar() 135 | 136 | var token uint8 137 | 138 | tokens := []uint8{T_SELECT, T_DISTINCT, T_FROM, T_WHERE, T_IN, T_NOT, T_COUNT, T_EOF} 139 | for i := range tokens { 140 | token, _ = Token() 141 | assertToken(t, token, tokens[i]) 142 | } 143 | } 144 | 145 | func TestNotReservedWords(t *testing.T) { 146 | setUp() 147 | 148 | source = "users commits" 149 | char = nextChar() 150 | 151 | var token uint8 152 | 153 | token, _ = Token() 154 | assertToken(t, token, T_ID) 155 | 156 | token, _ = Token() 157 | assertToken(t, token, T_ID) 158 | 159 | } 160 | 161 | func TestNumbers(t *testing.T) { 162 | setUp() 163 | 164 | source = "314 555" 165 | char = nextChar() 166 | 167 | var token uint8 168 | 169 | token, _ = Token() 170 | assertToken(t, token, T_NUMERIC) 171 | } 172 | 173 | func TestCurrentLexeme(t *testing.T) { 174 | setUp() 175 | source = "select * users" 176 | char = nextChar() 177 | 178 | var token uint8 179 | 180 | token, _ = Token() 181 | assertToken(t, token, T_SELECT) 182 | 183 | if CurrentLexeme != "select" { 184 | t.Errorf("%s is not select", CurrentLexeme) 185 | } 186 | 187 | token, _ = Token() 188 | assertToken(t, token, T_WILD_CARD) 189 | 190 | if CurrentLexeme != "*" { 191 | t.Errorf("%s is not *", CurrentLexeme) 192 | } 193 | 194 | token, _ = Token() 195 | assertToken(t, token, T_ID) 196 | 197 | if CurrentLexeme != "users" { 198 | t.Errorf("%s is not users", CurrentLexeme) 199 | } 200 | } 201 | 202 | func TestRepetitiveTokens(t *testing.T) { 203 | setUp() 204 | 205 | source = "select name, age from users" 206 | char = nextChar() 207 | 208 | var token uint8 209 | 210 | tokens := []uint8{T_SELECT, T_ID, T_COMMA, T_ID, T_FROM, T_ID} 211 | for i := range tokens { 212 | token, _ = Token() 213 | assertToken(t, token, tokens[i]) 214 | } 215 | } 216 | 217 | func TestReturningLiteral(t *testing.T) { 218 | setUp() 219 | 220 | source = " 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' " 221 | char = nextChar() 222 | 223 | token, error := Token() 224 | if error != nil { 225 | t.Errorf(error.Error()) 226 | } 227 | 228 | if token != T_LITERAL { 229 | t.Errorf("token should be literal") 230 | } 231 | 232 | if CurrentLexeme != "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" { 233 | t.Errorf("token should be e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") 234 | } 235 | } 236 | 237 | func TestEOFIntoLiteral(t *testing.T) { 238 | setUp() 239 | 240 | source = " 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 " 241 | char = nextChar() 242 | 243 | _, error := Token() 244 | if error == nil { 245 | t.Errorf("should throw error about unterminated literal") 246 | } 247 | } 248 | 249 | func TestReturningLiteralWithDoubleQuotes(t *testing.T) { 250 | setUp() 251 | 252 | source = " \"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391\" " 253 | char = nextChar() 254 | 255 | token, error := Token() 256 | if error != nil { 257 | t.Errorf(error.Error()) 258 | } 259 | 260 | if token != T_LITERAL { 261 | t.Errorf("token should be literal") 262 | } 263 | 264 | if CurrentLexeme != "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" { 265 | t.Errorf("token should be e69de29bb2d1d6434b8b29ae775ad8c2e48c5391") 266 | } 267 | } 268 | 269 | func TestUseTwoQuoteTypes(t *testing.T) { 270 | setUp() 271 | 272 | source = " \"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' " 273 | char = nextChar() 274 | 275 | _, error := Token() 276 | if error == nil { 277 | t.Errorf("should throw error with literal using two quote types") 278 | } 279 | } 280 | 281 | func assertToken(t *testing.T, expected uint8, found uint8) { 282 | if expected != found { 283 | t.Errorf("Token %s is not %s, lexeme: %s", TokenName(found), TokenName(expected), CurrentLexeme) 284 | } 285 | } 286 | 287 | func assertChar(t *testing.T, expected int32, found int32) { 288 | if found != expected { 289 | t.Errorf("Char '%s' is not '%s'", string(found), string(expected)) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /lexical/states.go: -------------------------------------------------------------------------------- 1 | package lexical 2 | 3 | const S_START = 0 4 | const S_WILD_CARD = 1 5 | const S_COMMA = 2 6 | const S_SEMICOLON = 3 7 | const S_GREATER = 4 8 | const S_SMALLER = 5 9 | const S_GREATER_OR_EQUAL = 6 10 | const S_SMALLER_OR_EQUAL = 7 11 | const S_SMALLER_EQUAL = 8 12 | const S_SMALLER_NOT_EQUAL = 9 13 | const S_EQUAL = 17 14 | const S_NOT_EQUAL = 18 15 | const S_LITERAL = 19 16 | const S_LITERAL_2 = 20 17 | const S_ID = 21 18 | const S_DISTINCT = 25 19 | const S_NUMERIC = 22 20 | const S_PARENTH_L = 23 21 | const S_PARENTH_R = 24 22 | -------------------------------------------------------------------------------- /lexical/tokens.go: -------------------------------------------------------------------------------- 1 | package lexical 2 | 3 | const T_SELECT = 1 4 | const T_DISTINCT = 29 5 | const T_FROM = 2 6 | const T_WHERE = 3 7 | const T_ORDER = 4 8 | const T_BY = 5 9 | const T_LIMIT = 6 10 | const T_DESC = 7 11 | const T_WILD_CARD = 8 12 | const T_COMMA = 9 13 | const T_SEMICOLON = 10 14 | const T_GREATER = 11 15 | const T_SMALLER = 12 16 | const T_GREATER_OR_EQUAL = 13 17 | const T_SMALLER_OR_EQUAL = 14 18 | const T_EQUAL = 15 19 | const T_NOT_EQUAL = 16 20 | const T_LITERAL = 17 21 | const T_NUMERIC = 18 22 | const T_AND = 19 23 | const T_OR = 20 24 | const T_ID = 21 25 | const T_PARENTH_L = 22 26 | const T_PARENTH_R = 23 27 | const T_IN = 24 28 | const T_ASC = 25 29 | const T_LIKE = 26 30 | const T_NOT = 27 31 | const T_COUNT = 28 32 | const T_EOF = 0 33 | const T_FUCK = 66 34 | const T_SHOW = 31 35 | const T_TABLES = 32 36 | const T_DATABASES = 33 37 | const T_USE = 34 38 | 39 | var tokenNamesByValue = map[uint8]string{ 40 | T_SELECT: "T_SELECT", 41 | T_DISTINCT: "T_DISTINCT", 42 | T_FROM: "T_FROM", 43 | T_WHERE: "T_WHERE", 44 | T_ORDER: "T_ORDER", 45 | T_BY: "T_BY", 46 | T_LIMIT: "T_LIMIT", 47 | T_DESC: "T_DESC", 48 | T_WILD_CARD: "T_WILD_CARD", 49 | T_COMMA: "T_COMMA", 50 | T_SEMICOLON: "T_SEMICOLON", 51 | T_GREATER: "T_GREATER", 52 | T_SMALLER: "T_SMALLER", 53 | T_GREATER_OR_EQUAL: "T_GREATER_OR_EQUAL", 54 | T_SMALLER_OR_EQUAL: "T_SMALLER_OR_EQUAL", 55 | T_EQUAL: "T_EQUAL", 56 | T_NOT_EQUAL: "T_NOT_EQUAL", 57 | T_LITERAL: "T_LITERAL", 58 | T_NUMERIC: "T_NUMERIC", 59 | T_ID: "T_ID", 60 | T_PARENTH_L: "T_PARENTH_L", 61 | T_PARENTH_R: "T_PARENTH_R", 62 | T_IN: "T_IN", 63 | T_EOF: "T_EOF", 64 | T_ASC: "T_ASC", 65 | T_NOT: "T_NOT", 66 | T_COUNT: "T_COUNT", 67 | T_SHOW: "T_SHOW", 68 | T_TABLES: "T_TABLES", 69 | T_DATABASES: "T_DATABASES", 70 | T_USE: "T_USE", 71 | } 72 | 73 | func TokenName(token uint8) string { 74 | return tokenNamesByValue[token] 75 | } 76 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/chzyer/readline" 10 | "github.com/cloudson/gitql/lexical" 11 | "github.com/cloudson/gitql/parser" 12 | "github.com/cloudson/gitql/runtime" 13 | "github.com/cloudson/gitql/semantical" 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | //go:embed version.txt 18 | var version string 19 | 20 | func main() { 21 | app := &cli.App{ 22 | Name: "gitql", 23 | Usage: "A git query language", 24 | Version: version, 25 | HideVersion: true, 26 | Flags: []cli.Flag{ 27 | &cli.BoolFlag{ 28 | Name: "interactive", 29 | Aliases: []string{"i"}, 30 | Usage: "Enter to interactive mode", 31 | }, 32 | &cli.StringFlag{ 33 | Name: "path", 34 | Aliases: []string{"p"}, 35 | Value: ".", 36 | Usage: `The (optional) path to run gitql`, 37 | }, 38 | &cli.StringFlag{ 39 | Name: "format", 40 | Aliases: []string{"f"}, 41 | Value: "table", 42 | Usage: "The output type format {table|json}", 43 | }, 44 | // for backward compatibility 45 | &cli.BoolFlag{ 46 | Name: "version", 47 | Aliases: []string{"v"}, 48 | Hidden: true, 49 | }, 50 | &cli.StringFlag{ 51 | Name: "type", 52 | Hidden: true, 53 | }, 54 | &cli.BoolFlag{ 55 | Name: "show-tables", 56 | Aliases: []string{"s"}, 57 | Hidden: true, 58 | }, 59 | }, 60 | Commands: []*cli.Command{ 61 | { 62 | Name: "show-tables", 63 | Aliases: []string{"s"}, 64 | Usage: "Show all tables", 65 | Action: showTablesCmd, 66 | }, 67 | { 68 | Name: "version", 69 | Aliases: []string{"v"}, 70 | Usage: "The version of gitql", 71 | Action: func(c *cli.Context) error { 72 | fmt.Printf("Gitql %s\n", version) 73 | return nil 74 | }, 75 | }, 76 | }, 77 | Action: func(c *cli.Context) error { 78 | path, format, interactive := c.String("path"), c.String("format"), c.Bool("interactive") 79 | 80 | // for backward compatibility 81 | if c.Bool("version") { 82 | fmt.Printf("Gitql %s\n", version) 83 | return nil 84 | } 85 | 86 | if c.Bool("show-tables") { 87 | return showTablesCmd(c) 88 | } 89 | 90 | if typ := c.String("type"); typ != "" { 91 | format = typ 92 | } 93 | // ============================ 94 | 95 | if c.NArg() == 0 && !interactive { 96 | return cli.ShowAppHelp(c) 97 | } 98 | 99 | if interactive { 100 | return runPrompt(path, format) 101 | } 102 | 103 | return runQuery(c.Args().First(), path, format) 104 | }, 105 | } 106 | 107 | if err := app.Run(os.Args); err != nil { 108 | fmt.Fprintf(os.Stderr, "Error: %s\n", err) 109 | os.Exit(1) 110 | } 111 | } 112 | 113 | func showTablesCmd(c *cli.Context) error { 114 | prog := &parser.NodeProgram{ 115 | Child: &parser.NodeShow{ 116 | Tables: true, 117 | }, 118 | } 119 | return runtime.RunShow(prog) 120 | } 121 | 122 | func runPrompt(folder, typeFormat string) error { 123 | term, err := readline.NewEx(&readline.Config{ 124 | Prompt: "gitql> ", 125 | AutoComplete: readline.SegmentFunc(suggestQuery), 126 | }) 127 | if err != nil { 128 | return err 129 | } 130 | defer term.Close() 131 | 132 | for { 133 | query, err := term.Readline() 134 | if err != nil { 135 | if err == io.EOF { 136 | break // Ctrl^D 137 | } 138 | return err 139 | } 140 | 141 | if query == "" { 142 | continue 143 | } 144 | 145 | if query == "exit" || query == "quit" { 146 | break 147 | } 148 | 149 | if err := runQuery(query, folder, typeFormat); err != nil { 150 | fmt.Println("Error: " + err.Error()) 151 | continue 152 | } 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func runQuery(query, folder, typeFormat string) error { 159 | parser.New(query) 160 | ast, err := parser.AST() 161 | if err != nil { 162 | return err 163 | } 164 | 165 | ast.Path = &folder 166 | switch lexical.Command { 167 | case lexical.T_SELECT: 168 | if err := semantical.Analysis(ast); err != nil { 169 | return err 170 | } 171 | err = runtime.RunSelect(ast, &typeFormat) 172 | break 173 | case lexical.T_SHOW: 174 | err = runtime.RunShow(ast) 175 | break 176 | case lexical.T_USE: 177 | err = runtime.RunUse(ast) 178 | break 179 | } 180 | 181 | return err 182 | } 183 | -------------------------------------------------------------------------------- /parser/ast.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/cloudson/gitql/lexical" 10 | ) 11 | 12 | type nodeMain interface { 13 | Run() 14 | } 15 | 16 | type nodeEmpty struct { 17 | } 18 | 19 | type NodeProgram struct { 20 | Child nodeMain 21 | Path *string 22 | } 23 | 24 | type NodeSelect struct { 25 | WildCard bool 26 | Count bool 27 | Distinct bool 28 | Fields []string 29 | Tables []string 30 | Where NodeExpr 31 | Order *NodeOrder 32 | Limit int 33 | } 34 | 35 | type NodeShow struct { 36 | Tables bool 37 | Databases bool 38 | } 39 | 40 | type NodeUse struct { 41 | Branch string 42 | } 43 | 44 | type NodeExpr interface { 45 | Assertion(lvalue, rvalue string) bool 46 | Operator() uint8 47 | LeftValue() NodeExpr 48 | RightValue() NodeExpr 49 | SetLeftValue(NodeExpr) 50 | SetRightValue(NodeExpr) 51 | } 52 | 53 | type nodeBinOp interface { 54 | LeftValue() NodeExpr 55 | RightValue() NodeExpr 56 | SetLeftValue(NodeExpr) 57 | SetRightValue(NodeExpr) 58 | } 59 | 60 | type nodeConst interface { 61 | SetValue(string) 62 | } 63 | 64 | type nodeAdapterBinToConst struct { 65 | adapted nodeBinOp 66 | } 67 | 68 | type NodeIn struct { 69 | leftValue NodeExpr 70 | rightValue NodeExpr 71 | Not bool 72 | } 73 | 74 | type NodeEqual struct { 75 | leftValue NodeExpr 76 | rightValue NodeExpr 77 | } 78 | 79 | type NodeNotEqual struct { 80 | leftValue NodeExpr 81 | rightValue NodeExpr 82 | } 83 | 84 | type NodeLike struct { 85 | leftValue NodeExpr 86 | rightValue NodeExpr 87 | Pattern *regexp.Regexp 88 | Not bool 89 | } 90 | 91 | type NodeGreater struct { 92 | leftValue NodeExpr 93 | rightValue NodeExpr 94 | Equal bool 95 | } 96 | 97 | type NodeSmaller struct { 98 | leftValue NodeExpr 99 | rightValue NodeExpr 100 | Equal bool 101 | } 102 | 103 | type NodeOr struct { 104 | leftValue NodeExpr 105 | rightValue NodeExpr 106 | } 107 | 108 | type NodeAnd struct { 109 | leftValue NodeExpr 110 | rightValue NodeExpr 111 | } 112 | 113 | type nodeNumber struct { 114 | value float64 115 | leftValue NodeExpr 116 | rightValue NodeExpr 117 | } 118 | 119 | type NodeLiteral struct { 120 | leftValue NodeExpr 121 | rightValue NodeExpr 122 | value string 123 | } 124 | 125 | type NodeId struct { 126 | leftValue NodeExpr 127 | rightValue NodeExpr 128 | value string 129 | } 130 | 131 | type NodeOrder struct { 132 | Field string 133 | Asc bool 134 | } 135 | 136 | func (s *NodeSelect) Run() { 137 | return 138 | } 139 | 140 | func (s *NodeShow) Run() { 141 | return 142 | } 143 | 144 | func (u *NodeUse) Run() { 145 | return 146 | } 147 | 148 | func (e *nodeEmpty) Run() { 149 | return 150 | } 151 | 152 | func (n *NodeIn) Assertion(lvalue string, rvalue string) bool { 153 | if n.Not { 154 | return !strings.Contains(rvalue, lvalue) 155 | } 156 | return strings.Contains(rvalue, lvalue) 157 | } 158 | 159 | func (n *NodeIn) SetLeftValue(e NodeExpr) { 160 | n.leftValue = e 161 | } 162 | 163 | func (n *NodeIn) SetRightValue(e NodeExpr) { 164 | n.rightValue = e 165 | } 166 | 167 | func (n *NodeIn) RightValue() NodeExpr { 168 | return n.rightValue 169 | } 170 | 171 | func (n *NodeIn) LeftValue() NodeExpr { 172 | return n.leftValue 173 | } 174 | 175 | func (n *NodeIn) Operator() uint8 { 176 | return lexical.T_IN 177 | } 178 | 179 | // EQUAL 180 | func (n *NodeEqual) Assertion(lvalue string, rvalue string) bool { 181 | if len(lvalue) == 40 { 182 | return lvalue[0:len(rvalue)] == rvalue 183 | } 184 | return lvalue == rvalue 185 | } 186 | 187 | func (n *NodeEqual) Operator() uint8 { 188 | return lexical.T_EQUAL 189 | } 190 | 191 | func (n *NodeEqual) SetLeftValue(e NodeExpr) { 192 | n.leftValue = e 193 | } 194 | 195 | func (n *NodeEqual) SetRightValue(e NodeExpr) { 196 | n.rightValue = e 197 | } 198 | 199 | func (n *NodeEqual) RightValue() NodeExpr { 200 | return n.rightValue 201 | } 202 | 203 | func (n *NodeEqual) LeftValue() NodeExpr { 204 | return n.leftValue 205 | } 206 | 207 | // NOT EQUAL 208 | func (n *NodeNotEqual) Assertion(lvalue string, rvalue string) bool { 209 | if len(lvalue) == 40 { 210 | return lvalue[0:len(rvalue)] != rvalue 211 | } 212 | return lvalue != rvalue 213 | } 214 | 215 | func (n *NodeNotEqual) Operator() uint8 { 216 | return lexical.T_NOT_EQUAL 217 | } 218 | 219 | func (n *NodeNotEqual) SetLeftValue(e NodeExpr) { 220 | n.leftValue = e 221 | } 222 | 223 | func (n *NodeNotEqual) SetRightValue(e NodeExpr) { 224 | n.rightValue = e 225 | } 226 | 227 | func (n *NodeNotEqual) RightValue() NodeExpr { 228 | return n.rightValue 229 | } 230 | 231 | func (n *NodeNotEqual) LeftValue() NodeExpr { 232 | return n.leftValue 233 | } 234 | 235 | // LIKE 236 | func (n *NodeLike) Assertion(lvalue string, rvalue string) bool { 237 | if n.Not { 238 | return !n.Pattern.MatchString(lvalue) 239 | } 240 | return n.Pattern.MatchString(lvalue) 241 | } 242 | 243 | func (n *NodeLike) Operator() uint8 { 244 | return lexical.T_LIKE 245 | } 246 | 247 | func (n *NodeLike) SetLeftValue(e NodeExpr) { 248 | n.leftValue = e 249 | } 250 | 251 | func (n *NodeLike) SetRightValue(e NodeExpr) { 252 | n.rightValue = e 253 | } 254 | 255 | func (n *NodeLike) RightValue() NodeExpr { 256 | return n.rightValue 257 | } 258 | 259 | func (n *NodeLike) LeftValue() NodeExpr { 260 | return n.leftValue 261 | } 262 | 263 | // GREATER 264 | func (n *NodeGreater) Assertion(lvalue string, rvalue string) bool { 265 | time := ExtractDate(rvalue) 266 | if time != nil { 267 | timeFound := ExtractDate(lvalue) 268 | if timeFound != nil { 269 | return timeFound.After(*time) || (n.Equal && timeFound.Equal(*time)) 270 | } 271 | } 272 | return lvalue > rvalue 273 | } 274 | 275 | func (n *NodeGreater) Operator() uint8 { 276 | return lexical.T_GREATER 277 | } 278 | 279 | func (n *NodeGreater) SetLeftValue(e NodeExpr) { 280 | n.leftValue = e 281 | } 282 | 283 | func (n *NodeGreater) SetRightValue(e NodeExpr) { 284 | n.rightValue = e 285 | } 286 | 287 | func (n *NodeGreater) RightValue() NodeExpr { 288 | return n.rightValue 289 | } 290 | 291 | func (n *NodeGreater) LeftValue() NodeExpr { 292 | return n.leftValue 293 | } 294 | 295 | // SMALLER 296 | func (n *NodeSmaller) Assertion(lvalue string, rvalue string) bool { 297 | time := ExtractDate(rvalue) 298 | if time != nil { 299 | timeFound := ExtractDate(lvalue) 300 | if timeFound != nil { 301 | return timeFound.Before(*time) || (n.Equal && timeFound.Equal(*time)) 302 | } 303 | } 304 | return lvalue < rvalue 305 | } 306 | 307 | func (n *NodeSmaller) Operator() uint8 { 308 | return lexical.T_SMALLER 309 | } 310 | 311 | func (n *NodeSmaller) SetLeftValue(e NodeExpr) { 312 | n.leftValue = e 313 | } 314 | 315 | func (n *NodeSmaller) SetRightValue(e NodeExpr) { 316 | n.rightValue = e 317 | } 318 | 319 | func (n *NodeSmaller) RightValue() NodeExpr { 320 | return n.rightValue 321 | } 322 | 323 | func (n *NodeSmaller) LeftValue() NodeExpr { 324 | return n.leftValue 325 | } 326 | 327 | // OR 328 | func (n *NodeOr) Assertion(lvalue string, rvalue string) bool { 329 | return false 330 | 331 | } 332 | 333 | func (n *NodeOr) Operator() uint8 { 334 | return lexical.T_OR 335 | } 336 | 337 | func (n *NodeOr) SetLeftValue(e NodeExpr) { 338 | n.leftValue = e 339 | } 340 | 341 | func (n *NodeOr) SetRightValue(e NodeExpr) { 342 | n.rightValue = e 343 | } 344 | 345 | func (n *NodeOr) RightValue() NodeExpr { 346 | return n.rightValue 347 | } 348 | 349 | func (n *NodeOr) LeftValue() NodeExpr { 350 | return n.leftValue 351 | } 352 | 353 | // AND 354 | func (n *NodeAnd) Assertion(lvalue string, rvalue string) bool { 355 | return lvalue == rvalue 356 | } 357 | 358 | func (n *NodeAnd) Operator() uint8 { 359 | return lexical.T_AND 360 | } 361 | 362 | func (n *NodeAnd) SetLeftValue(e NodeExpr) { 363 | n.leftValue = e 364 | } 365 | 366 | func (n *NodeAnd) SetRightValue(e NodeExpr) { 367 | n.rightValue = e 368 | } 369 | 370 | func (n *NodeAnd) RightValue() NodeExpr { 371 | return n.rightValue 372 | } 373 | 374 | func (n *NodeAnd) LeftValue() NodeExpr { 375 | return n.leftValue 376 | } 377 | 378 | // LITERAL 379 | func (n *NodeLiteral) Assertion(lvalue string, rvalue string) bool { 380 | return lvalue == rvalue 381 | } 382 | 383 | func (n *NodeLiteral) Operator() uint8 { 384 | return lexical.T_LITERAL 385 | } 386 | 387 | func (n *NodeLiteral) SetValue(value string) { 388 | n.value = value 389 | } 390 | 391 | func (n *NodeLiteral) SetLeftValue(e NodeExpr) { 392 | n.leftValue = e 393 | } 394 | 395 | func (n *NodeLiteral) SetRightValue(e NodeExpr) { 396 | n.rightValue = e 397 | } 398 | 399 | func (n *NodeLiteral) RightValue() NodeExpr { 400 | return n.rightValue 401 | } 402 | 403 | func (n *NodeLiteral) LeftValue() NodeExpr { 404 | return n.leftValue 405 | } 406 | 407 | func (n *NodeLiteral) Value() string { 408 | return n.value 409 | } 410 | 411 | // NUMBER 412 | func (n *nodeNumber) Assertion(lvalue string, rvalue string) bool { 413 | return lvalue == rvalue 414 | } 415 | 416 | func (n *nodeNumber) Operator() uint8 { 417 | return lexical.T_NUMERIC 418 | } 419 | 420 | func (n *nodeNumber) SetValue(value string) { 421 | n.value, _ = strconv.ParseFloat(value, 64) 422 | } 423 | 424 | func (n *nodeNumber) SetLeftValue(e NodeExpr) { 425 | n.leftValue = e 426 | } 427 | 428 | func (n *nodeNumber) SetRightValue(e NodeExpr) { 429 | n.rightValue = e 430 | } 431 | 432 | func (n *nodeNumber) RightValue() NodeExpr { 433 | return n.rightValue 434 | } 435 | 436 | func (n *nodeNumber) LeftValue() NodeExpr { 437 | return n.leftValue 438 | } 439 | 440 | func (n *nodeNumber) Value() float64 { 441 | return n.value 442 | } 443 | 444 | // ID 445 | func (n *NodeId) Assertion(lvalue string, rvalue string) bool { 446 | return lvalue == rvalue 447 | } 448 | 449 | func (n *NodeId) Operator() uint8 { 450 | return lexical.T_ID 451 | } 452 | 453 | func (n *NodeId) SetValue(value string) { 454 | n.value = value 455 | } 456 | 457 | func (n *NodeId) SetLeftValue(e NodeExpr) { 458 | n.leftValue = e 459 | } 460 | 461 | func (n *NodeId) SetRightValue(e NodeExpr) { 462 | n.rightValue = e 463 | } 464 | 465 | func (n *NodeId) RightValue() NodeExpr { 466 | return n.rightValue 467 | } 468 | 469 | func (n *NodeId) LeftValue() NodeExpr { 470 | return n.leftValue 471 | } 472 | 473 | func (n *NodeId) Value() string { 474 | return n.value 475 | } 476 | 477 | func (n *nodeAdapterBinToConst) setAdapted(a nodeBinOp) { 478 | n.adapted = a 479 | } 480 | 481 | func ExtractDate(date string) *time.Time { 482 | t, err := time.Parse(Time_YMD, date) 483 | if err == nil { 484 | return &t 485 | } 486 | 487 | t, err = time.Parse(Time_YMDHIS, date) 488 | if err == nil { 489 | return &t 490 | } 491 | 492 | // does not matter if the string is not a date 493 | // gitql will use it like a simple text 494 | return nil 495 | } 496 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | _ "unicode" 10 | 11 | "github.com/cloudson/gitql/lexical" 12 | ) 13 | 14 | var look_ahead uint8 15 | 16 | const Time_YMD = "2006-01-02" 17 | const Time_YMDHIS = "2006-01-02 15:04:05" 18 | 19 | type syntaxError struct { 20 | expected uint8 21 | found uint8 22 | } 23 | 24 | func (e *syntaxError) Error() string { 25 | var appendix = "" 26 | if e.found == lexical.T_LITERAL || e.found == lexical.T_ID { 27 | appendix = fmt.Sprintf("(%s)", lexical.CurrentLexeme) 28 | } 29 | return fmt.Sprintf("Expected %s and found %s%s", lexical.TokenName(e.expected), lexical.TokenName(e.found), appendix) 30 | } 31 | 32 | func throwSyntaxError(expectedToken uint8, foundToken uint8) error { 33 | error := new(syntaxError) 34 | error.expected = expectedToken 35 | error.found = foundToken 36 | 37 | return error 38 | } 39 | 40 | func New(source string) { 41 | lexical.New(source) 42 | } 43 | 44 | func AST() (*NodeProgram, error) { 45 | program := new(NodeProgram) 46 | node, err := gProgram() 47 | program.Child = node 48 | 49 | return program, err 50 | } 51 | 52 | func gProgram() (nodeMain, error) { 53 | token, tokenError := lexical.Token() 54 | look_ahead = token 55 | 56 | if tokenError != nil { 57 | return nil, tokenError 58 | } 59 | 60 | var node nodeMain 61 | var err error 62 | switch look_ahead { 63 | case lexical.T_SELECT: 64 | node, err = gSelect() 65 | break 66 | case lexical.T_SHOW: 67 | node, err = gShow() 68 | break 69 | case lexical.T_USE: 70 | node, err = gUse() 71 | break 72 | default: 73 | err = fmt.Errorf("invalid command") 74 | } 75 | if node == nil || err != nil { 76 | return nil, err 77 | } 78 | 79 | if look_ahead != lexical.T_EOF { 80 | return nil, throwSyntaxError(lexical.T_EOF, look_ahead) 81 | } 82 | 83 | return node, nil 84 | } 85 | 86 | func gSelect() (*NodeSelect, error) { 87 | token, tokenError := lexical.Token() 88 | look_ahead = token 89 | if tokenError != nil { 90 | return nil, tokenError 91 | } 92 | s := new(NodeSelect) 93 | 94 | // PARAMETERS 95 | isDistinct, fields, err := gTableParams() 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | if len(fields) == 1 { 101 | f0 := fields[0] 102 | if f0 == "*" { 103 | s.WildCard = true 104 | } else if f0 == "#" { 105 | s.Count = true 106 | } 107 | } 108 | s.Distinct = isDistinct 109 | s.Fields = fields 110 | 111 | // TABLES 112 | tables, err := gTableNames() 113 | if err != nil { 114 | return nil, err 115 | } 116 | s.Tables = tables 117 | 118 | // WHERE 119 | where, err := gWhere() 120 | if err != nil { 121 | return nil, err 122 | } 123 | s.Where = where 124 | 125 | // ORDER BY 126 | order, err := gOrder() 127 | if err != nil { 128 | return nil, err 129 | } 130 | s.Order = order 131 | 132 | // LIMIT 133 | s.Limit, err = gLimit() 134 | if s.Limit == -1 { 135 | // @todo search default limit from file config 136 | s.Limit = 10 137 | } 138 | 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | return s, nil 144 | } 145 | 146 | func gShow() (*NodeShow, error) { 147 | var err *lexical.TokenError 148 | look_ahead, err = lexical.Token() 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | node := new(NodeShow) 154 | switch look_ahead { 155 | case lexical.T_TABLES: 156 | node.Tables = true 157 | break 158 | case lexical.T_DATABASES: 159 | node.Databases = true 160 | break 161 | default: 162 | return nil, fmt.Errorf("can only show tables or databases") 163 | } 164 | look_ahead, _ = lexical.Token() 165 | return node, nil 166 | } 167 | 168 | func gUse() (*NodeUse, error) { 169 | var err *lexical.TokenError 170 | look_ahead, err = lexical.Token() 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | node := new(NodeUse) 176 | if look_ahead != lexical.T_ID { 177 | return nil, throwSyntaxError(lexical.T_ID, look_ahead) 178 | } 179 | 180 | node.Branch = lexical.CurrentLexeme 181 | look_ahead, _ = lexical.Token() 182 | return node, nil 183 | } 184 | 185 | func gTableNames() ([]string, error) { 186 | if look_ahead != lexical.T_FROM { 187 | return nil, throwSyntaxError(lexical.T_FROM, look_ahead) 188 | } 189 | token, error := lexical.Token() 190 | if error != nil { 191 | return nil, error 192 | } 193 | look_ahead = token 194 | if look_ahead != lexical.T_ID { 195 | return nil, throwSyntaxError(lexical.T_ID, look_ahead) 196 | } 197 | 198 | tables := make([]string, 1) 199 | tables[0] = lexical.CurrentLexeme 200 | 201 | token2, err := lexical.Token() 202 | if err != nil && token2 != lexical.T_EOF { 203 | return nil, err 204 | } 205 | look_ahead = token2 206 | 207 | return tables, nil 208 | } 209 | 210 | func gTableParams() (bool, []string, error) { 211 | if look_ahead == lexical.T_WILD_CARD { 212 | token, err := lexical.Token() 213 | if err != nil { 214 | return false, nil, err 215 | } 216 | look_ahead = token 217 | return false, []string{"*"}, nil 218 | } else if look_ahead == lexical.T_COUNT { 219 | result, err := gCount() 220 | return false, result, err 221 | } 222 | 223 | isDistinct := false 224 | if isDistinct = look_ahead == lexical.T_DISTINCT; isDistinct { 225 | token, err := lexical.Token() 226 | if err != nil { 227 | return false, nil, err 228 | } 229 | look_ahead = token 230 | } 231 | var fields = []string{} 232 | if look_ahead == lexical.T_ID { 233 | fields := append(fields, lexical.CurrentLexeme) 234 | token, err := lexical.Token() 235 | if err != nil { 236 | return isDistinct, nil, err 237 | } 238 | look_ahead = token 239 | fields, errorSyntax := gTableParamsRest(&fields, 1) 240 | 241 | return isDistinct, fields, errorSyntax 242 | } 243 | return isDistinct, nil, throwSyntaxError(lexical.T_ID, look_ahead) 244 | } 245 | 246 | // consume count(*) 247 | func gCount() ([]string, error) { 248 | // by construction, T_COUNT is consumed and stored 249 | // in the look_ahead 250 | err := gExactlyASpecificToken(lexical.T_COUNT) 251 | if err != nil { 252 | return nil, err 253 | } 254 | err = gExactlyASpecificToken(lexical.T_PARENTH_L) 255 | if err != nil { 256 | return nil, err 257 | } 258 | err = gExactlyASpecificToken(lexical.T_WILD_CARD) 259 | if err != nil { 260 | return nil, err 261 | } 262 | err = gExactlyASpecificToken(lexical.T_PARENTH_R) 263 | if err != nil { 264 | return nil, err 265 | } 266 | return []string{"#"}, nil 267 | } 268 | 269 | func gExactlyASpecificToken(expected uint8) error { 270 | if look_ahead != expected { 271 | return throwSyntaxError(expected, look_ahead) 272 | } 273 | token, err := lexical.Token() 274 | if err != nil { 275 | return err 276 | } 277 | look_ahead = token 278 | return nil 279 | } 280 | 281 | func gTableParamsRest(fields *[]string, count int) ([]string, error) { 282 | if lexical.T_COMMA == look_ahead { 283 | var errorToken *lexical.TokenError 284 | look_ahead, errorToken = lexical.Token() 285 | if errorToken != nil { 286 | return *fields, errorToken 287 | } 288 | if look_ahead != lexical.T_ID { 289 | return *fields, throwSyntaxError(lexical.T_ID, look_ahead) 290 | } 291 | 292 | n := append(*fields, lexical.CurrentLexeme) 293 | fields = &n 294 | look_ahead, errorToken = lexical.Token() 295 | if errorToken != nil { 296 | return *fields, errorToken 297 | } 298 | n, errorSyntax := gTableParamsRest(fields, count+1) 299 | fields = &n 300 | if errorSyntax != nil { 301 | return *fields, errorSyntax 302 | } 303 | } 304 | 305 | return *fields, nil 306 | } 307 | 308 | func gOrder() (*NodeOrder, error) { 309 | if look_ahead == lexical.T_ORDER { 310 | token, err := lexical.Token() 311 | if err != nil { 312 | return nil, err 313 | } 314 | if token != lexical.T_BY { 315 | return nil, throwSyntaxError(lexical.T_BY, token) 316 | } 317 | 318 | order := new(NodeOrder) 319 | token, err = lexical.Token() 320 | if err != nil { 321 | return nil, err 322 | } 323 | if token != lexical.T_ID { 324 | return nil, throwSyntaxError(lexical.T_ID, token) 325 | } 326 | order.Field = lexical.CurrentLexeme 327 | token, err = lexical.Token() 328 | if err != nil { 329 | return nil, err 330 | } 331 | if token != lexical.T_ASC && token != lexical.T_DESC { 332 | return nil, throwSyntaxError(lexical.T_ASC, token) 333 | } 334 | order.Asc = (token == lexical.T_ASC) 335 | 336 | token, err = lexical.Token() 337 | if err != nil { 338 | return nil, err 339 | } 340 | look_ahead = token 341 | return order, nil 342 | } 343 | 344 | return nil, nil 345 | } 346 | 347 | func gLimit() (int, error) { 348 | if look_ahead != lexical.T_LIMIT { 349 | return -1, nil 350 | } 351 | token, err := lexical.Token() 352 | if err != nil { 353 | return 0, err 354 | } 355 | look_ahead = token 356 | 357 | number, numberError := strconv.Atoi(lexical.CurrentLexeme) 358 | if numberError != nil { 359 | return 0, numberError 360 | } 361 | token2, err := lexical.Token() 362 | if token2 != lexical.T_EOF && err != nil { 363 | return 0, err 364 | } 365 | look_ahead = token2 366 | 367 | return number, nil 368 | } 369 | 370 | func gWhere() (NodeExpr, error) { 371 | if look_ahead != lexical.T_WHERE { 372 | return nil, nil 373 | } 374 | 375 | token, tokenError := lexical.Token() 376 | if tokenError != nil { 377 | return nil, tokenError 378 | } 379 | look_ahead = token 380 | conds, err := gWhereConds() 381 | 382 | return conds, err 383 | } 384 | 385 | func gWhereConds() (NodeExpr, error) { 386 | where, err := gWC2(false) 387 | if err != nil { 388 | return nil, err 389 | } 390 | 391 | return where, nil 392 | } 393 | 394 | // where cond OR 395 | func gWC2(eating bool) (NodeExpr, error) { 396 | if eating { 397 | token, err := lexical.Token() 398 | if token != lexical.T_EOF && err != nil { 399 | return nil, err 400 | } 401 | look_ahead = token 402 | } 403 | expr, err := gWC3(false) 404 | if err != nil { 405 | return nil, err 406 | } 407 | if look_ahead == lexical.T_OR { 408 | or := new(NodeOr) 409 | or.SetLeftValue(expr) 410 | expr2, err := gWC2(true) 411 | if err != nil { 412 | return nil, err 413 | } 414 | or.SetRightValue(expr2) 415 | return or, nil 416 | } 417 | return expr, nil 418 | } 419 | 420 | // where cond AND 421 | func gWC3(eating bool) (NodeExpr, error) { 422 | if eating { 423 | token, err := lexical.Token() 424 | if token != lexical.T_EOF && err != nil { 425 | return nil, err 426 | } 427 | look_ahead = token 428 | } 429 | expr, err := gWC4(false) 430 | if err != nil { 431 | return nil, err 432 | } 433 | if look_ahead == lexical.T_AND { 434 | and := new(NodeAnd) 435 | and.SetLeftValue(expr) 436 | expr2, err := gWC3(true) 437 | if err != nil { 438 | return nil, err 439 | } 440 | and.SetRightValue(expr2) 441 | return and, nil 442 | } 443 | return expr, nil 444 | } 445 | 446 | // where cond 'equal', 'in', 'not in', 'like', 'not like' and 'not equal' 447 | func gWC4(eating bool) (NodeExpr, error) { 448 | if eating { 449 | token, err := lexical.Token() 450 | if token != lexical.T_EOF && err != nil { 451 | return nil, err 452 | } 453 | look_ahead = token 454 | } 455 | expr, err := gWC5(false) 456 | if err != nil { 457 | return nil, err 458 | } 459 | 460 | var notBool bool 461 | if look_ahead == lexical.T_NOT { 462 | notBool = true 463 | token, err := lexical.Token() 464 | if err != nil { 465 | return nil, err 466 | } 467 | look_ahead = token 468 | if look_ahead != lexical.T_LIKE && look_ahead != lexical.T_IN { 469 | return nil, throwSyntaxError(lexical.T_NOT, look_ahead) 470 | } 471 | } 472 | 473 | switch look_ahead { 474 | case lexical.T_EQUAL: 475 | op := new(NodeEqual) 476 | op.SetLeftValue(expr) 477 | expr2, err := gWC4(true) 478 | if err != nil { 479 | return nil, err 480 | } 481 | op.SetRightValue(expr2) 482 | 483 | return op, nil 484 | case lexical.T_NOT_EQUAL: 485 | op := new(NodeNotEqual) 486 | op.SetLeftValue(expr) 487 | expr2, err := gWC4(true) 488 | if err != nil { 489 | return nil, err 490 | } 491 | op.SetRightValue(expr2) 492 | return op, nil 493 | case lexical.T_LIKE: 494 | op := new(NodeLike) 495 | op.SetLeftValue(expr) 496 | expr2, err := gWC4(true) 497 | if err != nil { 498 | return nil, err 499 | } 500 | op.SetRightValue(expr2) 501 | // Compile the regex while parsing, so that 502 | // we don't need to compile for every row 503 | literal, ok := expr2.(*NodeLiteral) 504 | if !ok { 505 | return nil, throwSyntaxError(lexical.T_LITERAL, look_ahead) 506 | } 507 | rx := strings.Replace(literal.Value(), "%", "(.*)", -1) 508 | op.Pattern, err = regexp.Compile(rx) 509 | op.Not = notBool 510 | return op, err 511 | case lexical.T_IN: 512 | op := new(NodeIn) 513 | op.SetLeftValue(expr) 514 | expr2, err := gWC4(true) 515 | if err != nil { 516 | return nil, err 517 | } 518 | op.SetRightValue(expr2) 519 | op.Not = notBool 520 | return op, nil 521 | } 522 | 523 | return expr, nil 524 | } 525 | 526 | // where cond greater and lesser 527 | func gWC5(eating bool) (NodeExpr, error) { 528 | if eating { 529 | token, err := lexical.Token() 530 | 531 | if token != lexical.T_EOF && err != nil { 532 | return nil, err 533 | } 534 | look_ahead = token 535 | } 536 | expr, err := rValue() 537 | if err != nil { 538 | return nil, err 539 | } 540 | 541 | switch look_ahead { 542 | case lexical.T_GREATER, lexical.T_GREATER_OR_EQUAL: 543 | op := new(NodeGreater) 544 | op.Equal = (look_ahead == lexical.T_GREATER_OR_EQUAL) 545 | op.SetLeftValue(expr) 546 | expr2, err := gWC5(true) 547 | if err != nil { 548 | return nil, err 549 | } 550 | op.SetRightValue(expr2) 551 | 552 | return op, nil 553 | case lexical.T_SMALLER, lexical.T_SMALLER_OR_EQUAL: 554 | op := new(NodeSmaller) 555 | op.Equal = (look_ahead == lexical.T_SMALLER_OR_EQUAL) 556 | op.SetLeftValue(expr) 557 | expr2, err := gWC5(true) 558 | if err != nil { 559 | return nil, err 560 | } 561 | op.SetRightValue(expr2) 562 | 563 | return op, nil 564 | } 565 | 566 | return expr, nil 567 | } 568 | 569 | func rValue() (NodeExpr, error) { 570 | if look_ahead == lexical.T_PARENTH_L { 571 | token, tokenError := lexical.Token() 572 | if tokenError != nil { 573 | return nil, tokenError 574 | } 575 | look_ahead = token 576 | conds, err := gWhereConds() 577 | if err != nil { 578 | return nil, err 579 | } 580 | if look_ahead != lexical.T_PARENTH_R { 581 | return nil, throwSyntaxError(lexical.T_PARENTH_R, look_ahead) 582 | } 583 | token2, tokenError := lexical.Token() 584 | if token2 != lexical.T_EOF && tokenError != nil { 585 | return nil, tokenError 586 | } 587 | look_ahead = token2 588 | 589 | return conds, nil 590 | } 591 | 592 | if look_ahead == lexical.T_ID { 593 | n := new(NodeId) 594 | n.SetValue(lexical.CurrentLexeme) 595 | 596 | token2, err := lexical.Token() 597 | if token2 != lexical.T_EOF && err != nil { 598 | return nil, err 599 | } 600 | look_ahead = token2 601 | 602 | return n, nil 603 | } 604 | 605 | lexeme := lexical.CurrentLexeme 606 | if look_ahead != lexical.T_LITERAL { 607 | return nil, errors.New("Only Literals and Date are allowed in `where` clause") 608 | } 609 | 610 | // @todo inserts IS NULL! 611 | 612 | n := new(NodeLiteral) 613 | n.SetValue(lexeme) 614 | token2, err := lexical.Token() 615 | if err != nil && token2 != lexical.T_EOF { 616 | return nil, err 617 | } 618 | look_ahead = token2 619 | 620 | return n, nil 621 | } 622 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestEmptySource(t *testing.T) { 10 | New("") 11 | ast, _ := AST() 12 | 13 | if reflect.TypeOf(ast) != reflect.TypeOf(new(NodeProgram)) { 14 | t.Errorf("AST should be a NodeProgram, found %s", reflect.TypeOf(ast).String()) 15 | } 16 | 17 | if ast.Child != nil { 18 | t.Errorf("Program is not empty") 19 | } 20 | } 21 | 22 | func TestInvalidFirstNode(t *testing.T) { 23 | New("cloudson") 24 | _, error := AST() 25 | 26 | if error == nil { 27 | t.Errorf("Expected a syntax error") 28 | } 29 | } 30 | 31 | func TestValidFirstNode(t *testing.T) { 32 | New("select * from users") 33 | ast, _ := AST() 34 | 35 | if ast.Child == nil { 36 | t.Errorf("Program is empty") 37 | } 38 | } 39 | 40 | func TestUsingWildCard(t *testing.T) { 41 | New("select * from users") 42 | ast, error := AST() 43 | 44 | if error != nil { 45 | t.Errorf(error.Error()) 46 | } 47 | 48 | if ast.Child == nil { 49 | t.Errorf("Program is empty") 50 | } 51 | 52 | selectNode := ast.Child.(*NodeSelect) 53 | if !selectNode.WildCard { 54 | t.Errorf("Expected wildcard setted") 55 | } 56 | } 57 | 58 | func TestUsingDistinct(t *testing.T) { 59 | New("select distinct author from commits") 60 | ast, error := AST() 61 | 62 | if error != nil { 63 | t.Errorf(error.Error()) 64 | } 65 | 66 | if ast.Child == nil { 67 | t.Errorf("Program is empty") 68 | } 69 | 70 | selectNode := ast.Child.(*NodeSelect) 71 | if !selectNode.Distinct { 72 | t.Errorf("Distinct was expected") 73 | } 74 | } 75 | 76 | func TestUsingCount(t *testing.T) { 77 | New("select count(*) from users") 78 | ast, error := AST() 79 | 80 | if error != nil { 81 | t.Errorf(error.Error()) 82 | } 83 | 84 | if ast.Child == nil { 85 | t.Errorf("Program is empty") 86 | } 87 | 88 | selectNode := ast.Child.(*NodeSelect) 89 | if !selectNode.Count { 90 | t.Errorf("Expected count setted") 91 | } 92 | } 93 | 94 | func TestUsingOneFieldName(t *testing.T) { 95 | New("select name from files") 96 | 97 | ast, error := AST() 98 | 99 | if error != nil { 100 | t.Errorf(error.Error()) 101 | } 102 | 103 | selectNode := ast.Child.(*NodeSelect) 104 | 105 | if len(selectNode.Fields) != 1 { 106 | t.Errorf("Expected exactly one field and found %d", len(selectNode.Fields)) 107 | } 108 | 109 | if selectNode.Fields[0] != "name" { 110 | t.Errorf("Expected param 'name' and found '%s'", selectNode.Fields[0]) 111 | } 112 | } 113 | 114 | func TestUsingFieldNames(t *testing.T) { 115 | New("select name, created_at from files") 116 | 117 | ast, error := AST() 118 | 119 | if error != nil { 120 | t.Errorf(error.Error()) 121 | } 122 | 123 | selectNode := ast.Child.(*NodeSelect) 124 | if len(selectNode.Fields) != 2 { 125 | t.Errorf("Expected exactly two fields and found %d", len(selectNode.Fields)) 126 | } 127 | } 128 | 129 | func TestWithOneTable(t *testing.T) { 130 | New("select name, created_at from files") 131 | 132 | ast, error := AST() 133 | 134 | if error != nil { 135 | t.Errorf(error.Error()) 136 | } 137 | 138 | selectNode := ast.Child.(*NodeSelect) 139 | if len(selectNode.Fields) != 2 { 140 | t.Errorf("Expected exactly two fields and found %d", len(selectNode.Fields)) 141 | } 142 | 143 | if selectNode.Tables[0] != "files" { 144 | t.Errorf("Expected table 'files', found %s", selectNode.Tables[0]) 145 | } 146 | } 147 | 148 | func TestErrorWithUnexpectedComma(t *testing.T) { 149 | New("select name, from files") 150 | 151 | _, error := AST() 152 | 153 | if error == nil { 154 | t.Errorf("Expected error 'Unexpected T_COMMA'") 155 | } 156 | } 157 | 158 | func TestErrorWithMalformedCount(t *testing.T) { 159 | New("select count(*]") 160 | 161 | _, error := AST() 162 | 163 | if error == nil { 164 | t.Errorf("Expected error 'Expected )'") 165 | } 166 | } 167 | 168 | func TestErrorWithInvalidRootNode(t *testing.T) { 169 | New("name from files") 170 | 171 | _, error := AST() 172 | if error == nil { 173 | t.Errorf("Expected error 'EXPECTED T_SELECT'") 174 | } 175 | 176 | } 177 | 178 | func TestErrorSqlWithoutTable(t *testing.T) { 179 | New("select name from ") 180 | 181 | _, error := AST() 182 | if error == nil { 183 | t.Errorf("Expected error 'EXPECTED table'") 184 | } 185 | } 186 | 187 | func TestWithLimit(t *testing.T) { 188 | New("select * from files limit 5") 189 | 190 | ast, error := AST() 191 | if error != nil { 192 | t.Errorf(error.Error()) 193 | } 194 | 195 | selectNode := ast.Child.(*NodeSelect) 196 | if selectNode.Limit != 5 { 197 | t.Errorf("Limit should be 5, found %d!!!", selectNode.Limit) 198 | } 199 | } 200 | 201 | func TestWithEmptyLimit(t *testing.T) { 202 | New("select * from files limit") 203 | 204 | _, error := AST() 205 | if error == nil { 206 | t.Errorf("Shoud throw error because limit has not value") 207 | } 208 | } 209 | 210 | func TestWithNonNumericLimit(t *testing.T) { 211 | New("select * from commits limit cloud") 212 | 213 | _, error := AST() 214 | if error == nil { 215 | t.Errorf("Shoud throw error because limit is not a number") 216 | } 217 | } 218 | 219 | func TestWithWhereSimpleEqualComparation(t *testing.T) { 220 | New("select * from commits where hash = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391' ") 221 | 222 | ast, err := AST() 223 | if err != nil { 224 | t.Errorf(err.Error()) 225 | } 226 | 227 | selectNode := ast.Child.(*NodeSelect) 228 | w := selectNode.Where 229 | if w == nil { 230 | t.Errorf("should has where node") 231 | } 232 | 233 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeEqual)) { 234 | t.Errorf("should be a NodeEqual") 235 | } 236 | 237 | lValue := w.LeftValue().(*NodeId) 238 | rValue := w.RightValue().(*NodeLiteral) 239 | if lValue.Value() != "hash" { 240 | t.Errorf("LValue should be 'hash'") 241 | } 242 | 243 | if rValue.Value() != "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" { 244 | t.Errorf("LValue should be 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'") 245 | } 246 | 247 | } 248 | 249 | func TestWhereWithNotEqualCompare(t *testing.T) { 250 | New("select * from commits where author != 'cloudson'") 251 | 252 | ast, err := AST() 253 | if err != nil { 254 | t.Errorf(err.Error()) 255 | return 256 | } 257 | 258 | selectNode := ast.Child.(*NodeSelect) 259 | w := selectNode.Where 260 | if w == nil { 261 | t.Errorf("should has where node") 262 | } 263 | 264 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeNotEqual)) { 265 | t.Errorf("should be a NodeNotEqual") 266 | } 267 | 268 | lValue := w.LeftValue().(*NodeId) 269 | rValue := w.RightValue().(*NodeLiteral) 270 | if lValue.Value() != "author" { 271 | t.Errorf("LValue should be 'author'") 272 | } 273 | 274 | if rValue.Value() != "cloudson" { 275 | t.Errorf("LValue should be 'cloudson'") 276 | } 277 | } 278 | 279 | func TestWhereWithIn(t *testing.T) { 280 | New("Select message from commits where 'react' in message") 281 | 282 | ast, err := AST() 283 | if err != nil { 284 | t.Errorf(err.Error()) 285 | return 286 | } 287 | 288 | selectNode := ast.Child.(*NodeSelect) 289 | w := selectNode.Where 290 | if w == nil { 291 | t.Errorf("should have where node") 292 | } 293 | 294 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeIn)) { 295 | t.Errorf("should be a NodeIn") 296 | } 297 | 298 | notBool := w.(*NodeIn).Not 299 | if notBool == true { 300 | t.Errorf("Not bool should be set to false") 301 | } 302 | 303 | lValue := w.LeftValue().(*NodeLiteral) 304 | rValue := w.RightValue().(*NodeId) 305 | if lValue.Value() != "react" { 306 | t.Errorf("LValue should be 'react'") 307 | } 308 | 309 | if rValue.Value() != "message" { 310 | t.Errorf("RValue should be 'message'") 311 | } 312 | 313 | } 314 | 315 | func TestWhereWithNotIn(t *testing.T) { 316 | New("Select message from commits where 'react' not in message") 317 | 318 | ast, err := AST() 319 | if err != nil { 320 | t.Errorf(err.Error()) 321 | return 322 | } 323 | 324 | selectNode := ast.Child.(*NodeSelect) 325 | w := selectNode.Where 326 | if w == nil { 327 | t.Errorf("should have where node") 328 | } 329 | 330 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeIn)) { 331 | t.Errorf("should be a NodeIn") 332 | } 333 | 334 | notBool := w.(*NodeIn).Not 335 | if notBool == false { 336 | t.Errorf("Not bool should be set to true") 337 | } 338 | 339 | lValue := w.LeftValue().(*NodeLiteral) 340 | rValue := w.RightValue().(*NodeId) 341 | if lValue.Value() != "react" { 342 | t.Errorf("LValue should be 'react'") 343 | } 344 | 345 | if rValue.Value() != "message" { 346 | t.Errorf("RValue should be 'message'") 347 | } 348 | 349 | } 350 | 351 | func TestWhereWithLike(t *testing.T) { 352 | New("Select author, message from commits where message like '%B'") 353 | 354 | ast, err := AST() 355 | if err != nil { 356 | t.Errorf(err.Error()) 357 | return 358 | } 359 | 360 | selectNode := ast.Child.(*NodeSelect) 361 | w := selectNode.Where 362 | if w == nil { 363 | t.Errorf("should have where node") 364 | } 365 | 366 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeLike)) { 367 | t.Errorf("should be a NodeLike") 368 | } 369 | 370 | notBool := w.(*NodeLike).Not 371 | if notBool == true { 372 | t.Errorf("Not bool should be set to false") 373 | } 374 | 375 | lValue := w.LeftValue().(*NodeId) 376 | rValue := w.RightValue().(*NodeLiteral) 377 | if lValue.Value() != "message" { 378 | t.Errorf("LValue should be 'message'") 379 | } 380 | 381 | es := `Rvalue should be &B` 382 | if rValue.Value() != "%B" { 383 | t.Errorf(es) 384 | } 385 | 386 | } 387 | 388 | func TestWhereWithNotLike(t *testing.T) { 389 | New("Select author, message from commits where message not like '%B'") 390 | 391 | ast, err := AST() 392 | if err != nil { 393 | t.Errorf(err.Error()) 394 | return 395 | } 396 | 397 | selectNode := ast.Child.(*NodeSelect) 398 | w := selectNode.Where 399 | if w == nil { 400 | t.Errorf("should have where node") 401 | } 402 | 403 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeLike)) { 404 | t.Errorf("should be a NodeLike") 405 | } 406 | 407 | notBool := w.(*NodeLike).Not 408 | if notBool == false { 409 | t.Errorf("Not bool should be set to true") 410 | } 411 | 412 | lValue := w.LeftValue().(*NodeId) 413 | rValue := w.RightValue().(*NodeLiteral) 414 | if lValue.Value() != "message" { 415 | t.Errorf("LValue should be 'message'") 416 | } 417 | 418 | es := `Rvalue should be &B` 419 | if rValue.Value() != "%B" { 420 | t.Errorf(es) 421 | } 422 | 423 | } 424 | 425 | func TestWhereWithGreater(t *testing.T) { 426 | New("select * from commits where date > '2014-05-12 00:00:00' ") 427 | 428 | ast, err := AST() 429 | if err != nil { 430 | t.Errorf(err.Error()) 431 | return 432 | } 433 | 434 | selectNode := ast.Child.(*NodeSelect) 435 | w := selectNode.Where 436 | 437 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeGreater)) { 438 | t.Errorf("should be a NodeGreater") 439 | } 440 | } 441 | 442 | func TestWhereWithSmaller(t *testing.T) { 443 | New("select * from commits where date <= '2014-05-12 00:00:00' ") 444 | 445 | ast, err := AST() 446 | if err != nil { 447 | t.Errorf(err.Error()) 448 | return 449 | } 450 | 451 | selectNode := ast.Child.(*NodeSelect) 452 | w := selectNode.Where 453 | 454 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeSmaller)) { 455 | t.Errorf("should be a NodeSmaller") 456 | } 457 | } 458 | 459 | func TestWhereWithOR(t *testing.T) { 460 | New("select * from commits where hash = 'e69de29' or date > 'now' ") 461 | 462 | ast, err := AST() 463 | if err != nil { 464 | t.Fatalf(err.Error()) 465 | } 466 | 467 | selectNode := ast.Child.(*NodeSelect) 468 | w := selectNode.Where 469 | 470 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeOr)) { 471 | t.Errorf("should be a NodeOr") 472 | } 473 | 474 | lValue := w.LeftValue().(*NodeEqual) 475 | rValue := w.RightValue().(*NodeGreater) 476 | 477 | if reflect.TypeOf(lValue) != reflect.TypeOf(new(NodeEqual)) { 478 | t.Errorf("should be a NodeEqual") 479 | } 480 | 481 | if reflect.TypeOf(rValue) != reflect.TypeOf(new(NodeGreater)) { 482 | t.Errorf("should be a NodeGreater") 483 | } 484 | } 485 | 486 | func TestInvalidchar(t *testing.T) { 487 | New("select * from commits where hash = 2 & date ") 488 | _, err := AST() 489 | if err == nil { 490 | t.Fatalf(err.Error()) 491 | } 492 | } 493 | 494 | func TestErrorIfOperatorBeforeEOF(t *testing.T) { 495 | New("select * from commits where hash = 'e69de29' and date > ") 496 | _, err := AST() 497 | if err == nil { 498 | t.Fatalf("Shoud be error with T_GREATER before T_EOF") 499 | } 500 | 501 | New("select * from commits where hash = 'e69de29' and ") 502 | _, err = AST() 503 | if err == nil { 504 | t.Fatalf("Shoud be error with T_AND before T_EOF") 505 | } 506 | 507 | // @todo should this sql throws an error ? 508 | New("select * from commits where hash = 'e69de29' and date ") 509 | } 510 | 511 | func TestConditionWithNoPrecedentParent(t *testing.T) { 512 | New("select * from commits where hash = 'e69de29' and date > 'now' or hash = 'fff3331'") 513 | 514 | ast, err := AST() 515 | if err != nil { 516 | t.Fatalf(err.Error()) 517 | } 518 | 519 | selectNode := ast.Child.(*NodeSelect) 520 | w := selectNode.Where 521 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeOr)) { 522 | t.Errorf("should be a NodeOr") 523 | } 524 | lValue := w.LeftValue().(*NodeAnd) 525 | rValue := w.RightValue().(*NodeEqual) 526 | 527 | if reflect.TypeOf(lValue) != reflect.TypeOf(new(NodeAnd)) { 528 | t.Errorf("should be a NodeAnd") 529 | } 530 | 531 | if reflect.TypeOf(rValue) != reflect.TypeOf(new(NodeEqual)) { 532 | t.Errorf("should be a NodeEqual") 533 | } 534 | } 535 | 536 | func TestConditionWithPrecedentParent(t *testing.T) { 537 | New("select * from commits where hash = 'e69de29' and (date > 'now' or hash = 'fff3331')") 538 | 539 | ast, err := AST() 540 | if err != nil { 541 | t.Fatalf(err.Error()) 542 | } 543 | 544 | selectNode := ast.Child.(*NodeSelect) 545 | w := selectNode.Where 546 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeAnd)) { 547 | t.Errorf("should be a NodeAnd") 548 | } 549 | lValue := w.LeftValue().(*NodeEqual) 550 | rValue := w.RightValue().(*NodeOr) 551 | 552 | if reflect.TypeOf(lValue) != reflect.TypeOf(new(NodeEqual)) { 553 | t.Errorf("should be a NodeEqual") 554 | } 555 | 556 | if reflect.TypeOf(rValue) != reflect.TypeOf(new(NodeOr)) { 557 | t.Errorf("should be a NodeOr") 558 | } 559 | } 560 | 561 | func TestUsingInOperator(t *testing.T) { 562 | New("select * from commits where hash in 'e69de29' ") 563 | 564 | ast, err := AST() 565 | if err != nil { 566 | t.Fatalf(err.Error()) 567 | } 568 | 569 | selectNode := ast.Child.(*NodeSelect) 570 | w := selectNode.Where 571 | if reflect.TypeOf(w) != reflect.TypeOf(new(NodeIn)) { 572 | t.Errorf("Where should be node in") 573 | } 574 | } 575 | 576 | func TestOrderByWithASC(t *testing.T) { 577 | New("select * from refs order by hash asc ") 578 | 579 | ast, err := AST() 580 | if err != nil { 581 | t.Fatalf(err.Error()) 582 | } 583 | 584 | selectNode := ast.Child.(*NodeSelect) 585 | o := selectNode.Order 586 | if o == nil { 587 | t.Errorf("should has order node") 588 | } 589 | } 590 | 591 | func TestOrderByWithDESC(t *testing.T) { 592 | New("select * from refs order by hash desc ") 593 | 594 | ast, err := AST() 595 | if err != nil { 596 | t.Fatalf(err.Error()) 597 | } 598 | 599 | selectNode := ast.Child.(*NodeSelect) 600 | o := selectNode.Order 601 | if o == nil { 602 | t.Errorf("should has order node") 603 | } 604 | } 605 | 606 | func TestOrderByWithoutASC(t *testing.T) { 607 | New("select * from refs order by hash ") 608 | 609 | _, err := AST() 610 | if err == nil { 611 | t.Fatalf("Shoud throws error about order by without asc/desc") 612 | } 613 | } 614 | 615 | func TestOrderByWithoutBY(t *testing.T) { 616 | New("select * from refs order hash asc ") 617 | 618 | _, err := AST() 619 | if err == nil { 620 | t.Fatalf("Shoud throws error about order by without by") 621 | } 622 | } 623 | 624 | func TestOrderByWithInvalidField(t *testing.T) { 625 | New("select * from refs order by 'hash' ") 626 | 627 | _, err := AST() 628 | if err == nil { 629 | t.Fatalf("Shoud throws error about order by with id instead of identifier") 630 | } 631 | } 632 | 633 | func TestExtractDate(t *testing.T) { 634 | cases := [][]string{ 635 | {"2014-04-09", "2014-04-09 00:00:00"}, 636 | {"2012-12-21 12:00:00", "2012-12-21 12:00:00"}, 637 | } 638 | 639 | for _, c := range cases { 640 | expected, err := time.Parse(Time_YMDHIS, c[1]) 641 | found := ExtractDate(c[0]) 642 | if err != nil || !expected.Equal(*found) { 643 | t.Errorf("Date %s should be %s", c[0], c[1]) 644 | } 645 | } 646 | } 647 | 648 | func TestExtractInvalidDate(t *testing.T) { 649 | cases := []string{ 650 | "cloudson", 651 | "2012-12-2112:00:00", 652 | } 653 | 654 | for _, c := range cases { 655 | found := ExtractDate(c) 656 | if found != nil { 657 | t.Errorf("Date %s should not be parsed to time", c) 658 | } 659 | } 660 | } 661 | 662 | func TestShowTables(t *testing.T) { 663 | New("show tables") 664 | ast, err := AST() 665 | node := ast.Child.(*NodeShow) 666 | 667 | if err != nil { 668 | t.Errorf("Error parsing 'show tables': %v", err) 669 | } 670 | 671 | if !node.Tables { 672 | t.Errorf("NodeShow.Tables should be true") 673 | } 674 | } 675 | 676 | func TestShowDatabases(t *testing.T) { 677 | New("show databases") 678 | ast, err := AST() 679 | node := ast.Child.(*NodeShow) 680 | 681 | if err != nil { 682 | t.Errorf("Error parsing 'show databases': %v", err) 683 | } 684 | 685 | if !node.Databases { 686 | t.Errorf("NodeShow.Databases should be true") 687 | } 688 | } 689 | 690 | func TestInvalidShow(t *testing.T) { 691 | cases := []string{ 692 | "show", 693 | "show invalid", 694 | "show tables show", 695 | "show databases show", 696 | "show tables show databases", 697 | } 698 | 699 | fail := false 700 | for _, c := range cases { 701 | New(c) 702 | _, err := AST() 703 | if err == nil { 704 | t.Logf("Input '%v' should fail", c) 705 | fail = true 706 | } 707 | } 708 | 709 | if fail { 710 | t.Fail() 711 | } 712 | } 713 | 714 | func TestUse(t *testing.T) { 715 | New("use master") 716 | ast, err := AST() 717 | node := ast.Child.(*NodeUse) 718 | 719 | if err != nil { 720 | t.Errorf("Error parsing 'use master': %v", err) 721 | } 722 | 723 | if node.Branch != "master" { 724 | t.Errorf("NodeUse.Branch should be 'master', is '%s'", node.Branch) 725 | } 726 | } 727 | 728 | func TestInvalidUse(t *testing.T) { 729 | cases := []string{ 730 | "use", 731 | "use master extra-param", 732 | } 733 | 734 | fail := false 735 | for _, c := range cases { 736 | New(c) 737 | _, err := AST() 738 | if err == nil { 739 | t.Logf("Input '%v' should fail", c) 740 | fail = true 741 | } 742 | } 743 | 744 | if fail { 745 | t.Fail() 746 | } 747 | } 748 | -------------------------------------------------------------------------------- /runtime/commits.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/cloudson/gitql/parser" 10 | "github.com/cloudson/gitql/utilities" 11 | "github.com/go-git/go-git/v5" 12 | "github.com/go-git/go-git/v5/plumbing/object" 13 | ) 14 | 15 | func walkCommits(n *parser.NodeProgram, visitor *RuntimeVisitor) (*TableData, error) { 16 | head, err := repo.Head() 17 | if err != nil { 18 | return nil, err 19 | } 20 | iter, err := repo.Log(&git.LogOptions{From: head.Hash()}) 21 | 22 | s := n.Child.(*parser.NodeSelect) 23 | where := s.Where 24 | 25 | counter := 1 26 | fields := s.Fields 27 | if s.WildCard { 28 | fields = builder.possibleTables[s.Tables[0]] 29 | } 30 | resultFields := fields // These are the fields in output with wildcards expanded 31 | rows := make([]tableRow, s.Limit) 32 | usingOrder := false 33 | if s.Order != nil && !s.Count { 34 | usingOrder = true 35 | // Check if the order by field is in the selected fields. If not, add them to selected fields list 36 | if !utilities.IsFieldPresentInArray(fields, s.Order.Field) { 37 | fields = append(fields, s.Order.Field) 38 | } 39 | } 40 | 41 | // holds the seen values so far. field -> (value -> wasSeen) 42 | seen := make(map[string]map[string]bool) 43 | iter.ForEach(func(commit *object.Commit) error { 44 | builder.setCommit(commit) 45 | boolRegister = true 46 | visitor.VisitExpr(where) 47 | 48 | if boolRegister { 49 | isNew := true 50 | if !s.Count { 51 | newRow := make(tableRow) 52 | 53 | for _, f := range fields { 54 | data := metadataCommit(f, commit) 55 | 56 | if _, ok := seen[f]; !ok { 57 | seen[f] = make(map[string]bool) 58 | } 59 | 60 | isNew = !seen[f][data] 61 | 62 | newRow[f] = data 63 | seen[f][data] = true 64 | } 65 | 66 | if isNew || !s.Distinct { 67 | counter = counter + 1 68 | rows = append(rows, newRow) 69 | } 70 | } else { 71 | counter = counter + 1 72 | } 73 | } 74 | 75 | if !usingOrder && !s.Count && counter > s.Limit { 76 | return fmt.Errorf("limit") // stop iteration 77 | } 78 | 79 | return nil 80 | }) 81 | 82 | if s.Count { 83 | newRow := make(tableRow) 84 | // counter was started from 1! 85 | newRow[COUNT_FIELD_NAME] = strconv.Itoa(counter - 1) 86 | counter = 2 87 | rows = append(rows, newRow) 88 | } 89 | 90 | rowsSliced := rows[len(rows)-counter+1:] 91 | rowsSliced, err = orderTable(rowsSliced, s.Order) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | if usingOrder && !s.Count && counter > s.Limit { 97 | counter = s.Limit 98 | rowsSliced = rowsSliced[0:counter] 99 | } 100 | 101 | tableData := new(TableData) 102 | tableData.rows = rowsSliced 103 | tableData.fields = resultFields 104 | 105 | return tableData, nil 106 | } 107 | 108 | func metadataCommit(identifier string, commit *object.Commit) string { 109 | key := "" 110 | for key, _ = range builder.tables { 111 | break 112 | } 113 | table := key 114 | err := builder.UseFieldFromTable(identifier, table) 115 | if err != nil { 116 | log.Fatalln(err) 117 | } 118 | 119 | switch identifier { 120 | case "hash": 121 | return commit.ID().String() 122 | case "author": 123 | return commit.Author.Name 124 | case "author_email": 125 | return commit.Author.Email 126 | case "committer": 127 | return commit.Committer.Name 128 | case "committer_email": 129 | return commit.Committer.Email 130 | case "date": 131 | //return object.Committer().When.Format() 132 | return commit.Author.When.Format(parser.Time_YMDHIS) 133 | case "full_message": 134 | return commit.Message 135 | case "message": 136 | // return first line of a commit message 137 | message := commit.Message 138 | r := []rune("\n") 139 | idx := strings.IndexRune(message, r[0]) 140 | if idx != -1 { 141 | message = message[0:idx] 142 | } 143 | return message 144 | 145 | } 146 | log.Fatalf("Field %s not implemented yet \n", identifier) 147 | 148 | return "" 149 | } 150 | -------------------------------------------------------------------------------- /runtime/commits_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/cloudson/gitql/parser" 9 | "github.com/cloudson/gitql/semantical" 10 | ) 11 | 12 | func failTestIfError(err error, t *testing.T) { 13 | if err != nil { 14 | t.Error(err.Error()) 15 | } 16 | } 17 | 18 | func getTableForQuery(query, directory string, t *testing.T) *TableData { 19 | parser.New(query) 20 | ast, errGit := parser.AST() 21 | failTestIfError(errGit, t) 22 | 23 | folder, errFile := filepath.Abs(directory) 24 | failTestIfError(errFile, t) 25 | ast.Path = &folder 26 | errGit = semantical.Analysis(ast) 27 | failTestIfError(errGit, t) 28 | 29 | builder = GetGitBuilder(ast.Path) 30 | visitor := new(RuntimeVisitor) 31 | err := visitor.Visit(ast) 32 | failTestIfError(err, t) 33 | findWalkType(ast) 34 | tableData, err := walkCommits(ast, visitor) 35 | failTestIfError(err, t) 36 | return tableData 37 | } 38 | func TestSortOrdering(t *testing.T) { 39 | query := "select hash, date from commits order by date desc limit 3" 40 | tableData := getTableForQuery(query, "../", t) 41 | for i := 1; i < len(tableData.rows); i++ { 42 | if tableData.rows[i]["date"].(string) > tableData.rows[i-1]["date"].(string) { 43 | t.Errorf("Date not sored. row %d is bigger than row %d", i, i-1) 44 | } 45 | } 46 | 47 | queryWithoutDate := "select hash from commits order by date desc limit 3" 48 | tableDataNew := getTableForQuery(queryWithoutDate, "../", t) 49 | if len(tableData.rows) != len(tableDataNew.rows) { 50 | t.Error("Two queried returned different number of rows") 51 | } 52 | for i := 0; i < len(tableData.rows); i++ { 53 | if tableDataNew.rows[i]["hash"].(string) != tableData.rows[i]["hash"].(string) { 54 | t.Errorf("Data in row %d does not match on both tables", i) 55 | } 56 | } 57 | } 58 | 59 | func TestRowLimitsCount(t *testing.T) { 60 | query := "select hash, date from commits order by date desc limit 3" 61 | tableData := getTableForQuery(query, "../", t) 62 | 63 | if len(tableData.rows) > 3 { 64 | t.Error("Got more rows than the limit ") 65 | } 66 | } 67 | 68 | func TestWildcardFieldsCount(t *testing.T) { 69 | query := "select * from commits" 70 | table := getTableForQuery(query, "../", t) 71 | if len(table.fields) != 8 { 72 | t.Errorf("Commits has 8 fields. Output table got %d fields", len(table.fields)) 73 | } 74 | } 75 | 76 | func TestSelectedFieldsCount(t *testing.T) { 77 | query := "select author, hash from commits" 78 | table := getTableForQuery(query, "../", t) 79 | if len(table.fields) != 2 { 80 | t.Errorf("Selected 2 fields. Output table got %d fields", len(table.fields)) 81 | } 82 | if table.fields[0] != "author" || table.fields[1] != "hash" { 83 | t.Errorf("Selected 'author' and 'hash'. Got %v", table.fields) 84 | } 85 | } 86 | 87 | func TestWhereLike(t *testing.T) { 88 | query := "select hash, author from commits where hash like '%8813f1c5e6f5d10ef%'" 89 | table := getTableForQuery(query, "../", t) 90 | if len(table.rows) != 1 { 91 | t.Errorf("Expecting 1 row. Got %d rows", len(table.rows)) 92 | } 93 | } 94 | 95 | func TestNotEqualsInWhereLTGT(t *testing.T) { 96 | queryData := "select committer, hash from commits limit 1" 97 | table := getTableForQuery(queryData, "../", t) 98 | firstCommitter := table.rows[0]["committer"].(string) 99 | query := fmt.Sprintf("select committer, hash from commits where committer <> '%s' limit 1", firstCommitter) 100 | table = getTableForQuery(query, "../", t) 101 | if firstCommitter == table.rows[0]["committer"].(string) { 102 | t.Errorf("Still got the same committer as the first one. - %s", firstCommitter) 103 | } 104 | } 105 | 106 | func TestNotEqualsInWhere(t *testing.T) { 107 | queryData := "select committer, hash from commits limit 1" 108 | table := getTableForQuery(queryData, "../", t) 109 | firstCommitter := table.rows[0]["committer"].(string) 110 | query := fmt.Sprintf("select committer, hash from commits where committer != '%s' limit 1", firstCommitter) 111 | table = getTableForQuery(query, "../", t) 112 | if firstCommitter == table.rows[0]["committer"].(string) { 113 | t.Errorf("Still got the same committer as the first one. - %s", firstCommitter) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /runtime/reference.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | 8 | "github.com/cloudson/gitql/parser" 9 | "github.com/go-git/go-git/v5/plumbing" 10 | ) 11 | 12 | func walkReferences(n *parser.NodeProgram, visitor *RuntimeVisitor) (*TableData, error) { 13 | s := n.Child.(*parser.NodeSelect) 14 | where := s.Where 15 | 16 | // @TODO make PR with Repository.WalkReference() 17 | iterator, err := builder.repo.References() 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | counter := 1 23 | fields := s.Fields 24 | if s.WildCard { 25 | fields = builder.possibleTables[s.Tables[0]] 26 | } 27 | 28 | rows := make([]tableRow, s.Limit) 29 | usingOrder := false 30 | if s.Order != nil && !s.Count { 31 | usingOrder = true 32 | } 33 | 34 | iterator.ForEach(func(ref *plumbing.Reference) error { 35 | builder.setReference(ref) 36 | boolRegister = true 37 | visitor.VisitExpr(where) 38 | 39 | if boolRegister { 40 | fields := s.Fields 41 | 42 | if s.WildCard { 43 | fields = builder.possibleTables[s.Tables[0]] 44 | } 45 | 46 | if !s.Count { 47 | newRow := make(tableRow) 48 | for _, f := range fields { 49 | newRow[f] = metadataReference(f, ref) 50 | } 51 | rows = append(rows, newRow) 52 | } 53 | 54 | counter = counter + 1 55 | if !usingOrder && counter > s.Limit { 56 | return fmt.Errorf("limit") // stop iteration 57 | } 58 | } 59 | 60 | return nil 61 | }) 62 | 63 | if s.Count { 64 | newRow := make(tableRow) 65 | // counter was started from 1! 66 | newRow[COUNT_FIELD_NAME] = strconv.Itoa(counter - 1) 67 | counter = 2 68 | rows = append(rows, newRow) 69 | } 70 | 71 | rowsSliced := rows[len(rows)-counter+1:] 72 | rowsSliced, err = orderTable(rowsSliced, s.Order) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | if usingOrder && counter > s.Limit { 78 | counter = s.Limit 79 | rowsSliced = rowsSliced[0:counter] 80 | } 81 | 82 | tableData := new(TableData) 83 | tableData.rows = rowsSliced 84 | tableData.fields = fields 85 | 86 | return tableData, nil 87 | } 88 | 89 | func metadataReference(identifier string, ref *plumbing.Reference) string { 90 | key := "" 91 | for key, _ = range builder.tables { 92 | break 93 | } 94 | table := key 95 | err := builder.UseFieldFromTable(identifier, table) 96 | if err != nil { 97 | log.Fatalln(err) 98 | } 99 | switch identifier { 100 | case "name": 101 | return ref.Name().Short() 102 | case "full_name": 103 | return ref.Name().String() 104 | case "hash": 105 | target := ref.Hash() 106 | if target.IsZero() { 107 | return "NULL" 108 | } 109 | return target.String() 110 | case "type": 111 | if ref.Name().IsBranch() { 112 | return REFERENCE_TYPE_BRANCH 113 | } 114 | 115 | if ref.Name().IsTag() { 116 | return REFERENCE_TYPE_TAG 117 | } 118 | 119 | return "stash" // unknow 120 | } 121 | log.Fatalf("Field %s not implemented yet in reference\n", identifier) 122 | 123 | return "" 124 | } 125 | -------------------------------------------------------------------------------- /runtime/remotes.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | -------------------------------------------------------------------------------- /runtime/runtime.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "encoding/json" 10 | 11 | "github.com/cloudson/gitql/parser" 12 | "github.com/cloudson/gitql/semantical" 13 | "github.com/go-git/go-git/v5" 14 | "github.com/go-git/go-git/v5/config" 15 | "github.com/go-git/go-git/v5/plumbing" 16 | "github.com/go-git/go-git/v5/plumbing/object" 17 | "github.com/olekukonko/tablewriter" 18 | ) 19 | 20 | const ( 21 | WALK_COMMITS = 1 22 | WALK_REFERENCES = 2 23 | ) 24 | 25 | const ( 26 | REFERENCE_TYPE_BRANCH = "branch" 27 | REFERENCE_TYPE_REMOTE = "remote" 28 | REFERENCE_TYPE_TAG = "tag" 29 | ) 30 | 31 | const ( 32 | COUNT_FIELD_NAME = "count" 33 | ) 34 | 35 | var repo *git.Repository 36 | var builder *GitBuilder 37 | var boolRegister bool 38 | 39 | type tableRow map[string]interface{} 40 | type proxyTable struct { 41 | table string 42 | fields map[string]string 43 | } 44 | 45 | type GitBuilder struct { 46 | tables map[string]string 47 | possibleTables map[string][]string 48 | proxyTables map[string]*proxyTable 49 | repo *git.Repository 50 | currentWalkType uint8 51 | currentCommit *object.Commit 52 | 53 | currentReference *plumbing.Reference 54 | //walk *object.RevWalk 55 | } 56 | 57 | type RuntimeError struct { 58 | code uint8 59 | message string 60 | } 61 | 62 | type RuntimeVisitor struct { 63 | semantical.Visitor 64 | } 65 | 66 | type TableData struct { 67 | rows []tableRow 68 | fields []string 69 | } 70 | 71 | // =========================== Error 72 | 73 | func (e *RuntimeError) Error() string { 74 | return e.message 75 | } 76 | 77 | func throwRuntimeError(message string, code uint8) *RuntimeError { 78 | e := new(RuntimeError) 79 | e.message = message 80 | e.code = code 81 | 82 | return e 83 | } 84 | 85 | // =========================== Runtime 86 | func RunSelect(n *parser.NodeProgram, typeFormat *string) error { 87 | builder = GetGitBuilder(n.Path) 88 | visitor := new(RuntimeVisitor) 89 | err := visitor.Visit(n) 90 | if err != nil { 91 | return err 92 | } 93 | var tableData *TableData 94 | 95 | switch findWalkType(n) { 96 | case WALK_COMMITS: 97 | tableData, err = walkCommits(n, visitor) 98 | break 99 | case WALK_REFERENCES: 100 | tableData, err = walkReferences(n, visitor) 101 | break 102 | } 103 | 104 | if err != nil { 105 | return err 106 | } 107 | 108 | if *typeFormat == "json" { 109 | printJson(tableData) 110 | } else { 111 | printTable(tableData) 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func RunShow(node *parser.NodeProgram) error { 118 | s := node.Child.(*parser.NodeShow) 119 | if s.Databases { 120 | builder = GetGitBuilder(node.Path) 121 | fmt.Print("Databases: \n\n") 122 | databases, err := possibleDatabases() 123 | if err != nil { 124 | return err 125 | } 126 | for _, database := range databases { 127 | fmt.Println(database) 128 | } 129 | return nil 130 | } else if s.Tables { 131 | fmt.Print("Tables: \n\n") 132 | for tableName, fields := range PossibleTables() { 133 | fmt.Printf("%s\n\t", tableName) 134 | for i, field := range fields { 135 | comma := "." 136 | if i+1 < len(fields) { 137 | comma = ", " 138 | } 139 | fmt.Printf("%s%s", field, comma) 140 | } 141 | fmt.Println() 142 | } 143 | } 144 | return nil 145 | } 146 | 147 | func RunUse(node *parser.NodeProgram) error { 148 | builder = GetGitBuilder(node.Path) 149 | u := node.Child.(*parser.NodeUse) 150 | 151 | w, err := repo.Worktree() 152 | if err != nil { 153 | return err 154 | } 155 | 156 | s, err := w.Status() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if !s.IsClean() { 162 | return fmt.Errorf("worktree is not clean") 163 | } 164 | 165 | refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", u.Branch)) 166 | cOp := &git.CheckoutOptions{ 167 | Branch: refName, 168 | Create: false, 169 | } 170 | err = w.Checkout(cOp) 171 | if err != nil { 172 | // Try fetching branch from origin and then switching to it. 173 | // If it doesn't work, return the original error. 174 | remote, remoteErr := repo.Remote("origin") 175 | if remoteErr != nil { 176 | return err 177 | } 178 | remoteErr = remote.Fetch(&git.FetchOptions{ 179 | RefSpecs: []config.RefSpec{ 180 | config.RefSpec(fmt.Sprintf("%s:%s", refName, refName)), 181 | }, 182 | }) 183 | if remoteErr != nil { 184 | return err 185 | } 186 | err = w.Checkout(cOp) 187 | } 188 | 189 | if err == nil { 190 | fmt.Println("switched to database", u.Branch) 191 | } 192 | 193 | return err 194 | } 195 | 196 | func findWalkType(n *parser.NodeProgram) uint8 { 197 | s := n.Child.(*parser.NodeSelect) 198 | switch s.Tables[0] { 199 | case "commits": 200 | builder.currentWalkType = WALK_COMMITS 201 | case "refs", "tags", "branches": 202 | builder.currentWalkType = WALK_REFERENCES 203 | } 204 | 205 | return builder.currentWalkType 206 | } 207 | 208 | func printTable(tableData *TableData) { 209 | table := tablewriter.NewWriter(os.Stdout) 210 | table.SetAutoFormatHeaders(false) 211 | table.SetHeader(tableData.fields) 212 | table.SetRowLine(true) 213 | for _, row := range tableData.rows { 214 | rowData := make([]string, len(tableData.fields)) 215 | for i, field := range tableData.fields { 216 | rowData[i] = fmt.Sprintf("%v", row[field]) 217 | } 218 | table.Append(rowData) 219 | } 220 | table.Render() 221 | } 222 | 223 | func printJson(tableData *TableData) error { 224 | res, err := json.Marshal(tableData.rows) 225 | if err != nil { 226 | return throwRuntimeError(fmt.Sprintf("Json error:'%s'", err), 0) 227 | } else { 228 | fmt.Println(string(res)) 229 | } 230 | return nil 231 | } 232 | 233 | func orderTable(rows []tableRow, order *parser.NodeOrder) ([]tableRow, error) { 234 | if order == nil { 235 | return rows, nil 236 | } 237 | // We will use parser.NodeGreater.Assertion(A, B) to know if 238 | // A > B and then switch their positions. 239 | // Unfortunaly, we will use bubble sort, that is O(n²) 240 | // @todo change to quick or other better sort. 241 | var orderer parser.NodeExpr 242 | if order.Asc { 243 | orderer = new(parser.NodeGreater) 244 | } else { 245 | orderer = new(parser.NodeSmaller) 246 | } 247 | 248 | field := order.Field 249 | key := "" 250 | for key, _ = range builder.tables { 251 | break 252 | } 253 | table := key 254 | err := builder.UseFieldFromTable(field, table) 255 | if err != nil { 256 | return nil, err 257 | } 258 | 259 | for i, row := range rows { 260 | for j, rowWalk := range rows { 261 | if orderer.Assertion(fmt.Sprintf("%v", rowWalk[field]), fmt.Sprintf("%v", row[field])) { 262 | aux := rows[j] 263 | rows[j] = rows[i] 264 | rows[i] = aux 265 | } 266 | } 267 | } 268 | 269 | return rows, nil 270 | } 271 | 272 | func metadata(identifier string) string { 273 | switch builder.currentWalkType { 274 | case WALK_COMMITS: 275 | return metadataCommit(identifier, builder.currentCommit) 276 | case WALK_REFERENCES: 277 | return metadataReference(identifier, builder.currentReference) 278 | } 279 | 280 | log.Fatalln("GOD!") 281 | 282 | return "" 283 | } 284 | 285 | // =================== GitBuilder 286 | 287 | func GetGitBuilder(path *string) *GitBuilder { 288 | 289 | gb := new(GitBuilder) 290 | gb.tables = make(map[string]string) 291 | possibleTables := PossibleTables() 292 | gb.possibleTables = possibleTables 293 | 294 | proxyTables := map[string]*proxyTable{ 295 | "tags": proxyTableEntry("refs", map[string]string{"type": "tag"}), 296 | "branches": proxyTableEntry("refs", map[string]string{"type": "branch"}), 297 | } 298 | gb.proxyTables = proxyTables 299 | 300 | openRepository(path) 301 | 302 | gb.repo = repo 303 | 304 | return gb 305 | } 306 | 307 | func proxyTableEntry(t string, f map[string]string) *proxyTable { 308 | p := new(proxyTable) 309 | p.table = t 310 | p.fields = f 311 | 312 | return p 313 | } 314 | 315 | func openRepository(path *string) { 316 | _repo, err := git.PlainOpen(*path) 317 | if err != nil { 318 | log.Fatalln(err) 319 | } 320 | repo = _repo 321 | } 322 | 323 | func (g *GitBuilder) setCommit(object *object.Commit) { 324 | g.currentCommit = object 325 | } 326 | 327 | func (g *GitBuilder) setReference(object *plumbing.Reference) { 328 | g.currentReference = object 329 | } 330 | 331 | func (g *GitBuilder) WithTable(tableName string, alias string) error { 332 | err := g.isValidTable(tableName) 333 | if err != nil { 334 | return err 335 | } 336 | 337 | if g.possibleTables[tableName] == nil { 338 | return throwRuntimeError(fmt.Sprintf("Table '%s' not found", tableName), 0) 339 | } 340 | 341 | if alias == "" { 342 | alias = tableName 343 | } 344 | 345 | g.tables[alias] = tableName 346 | 347 | return nil 348 | } 349 | 350 | func (g *GitBuilder) isProxyTable(tableName string) bool { 351 | _, isIn := g.proxyTables[tableName] 352 | 353 | return isIn 354 | } 355 | 356 | func PossibleTables() map[string][]string { 357 | return map[string][]string{ 358 | "commits": { 359 | "hash", 360 | "date", 361 | "author", 362 | "author_email", 363 | "committer", 364 | "committer_email", 365 | "message", 366 | "full_message", 367 | }, 368 | "refs": { 369 | "name", 370 | "full_name", 371 | "type", 372 | "hash", 373 | }, 374 | "tags": { 375 | "name", 376 | "full_name", 377 | "hash", 378 | }, 379 | "branches": { 380 | "name", 381 | "full_name", 382 | "hash", 383 | }, 384 | } 385 | } 386 | 387 | func possibleDatabases() ([]string, error) { 388 | // local branches 389 | iter, err := repo.Branches() 390 | if err != nil { 391 | return nil, err 392 | } 393 | 394 | branches := make([]string, 0) 395 | iter.ForEach(func(r *plumbing.Reference) error { 396 | if r.Name().IsBranch() { 397 | branches = append(branches, r.Name().Short()) 398 | } 399 | return nil 400 | }) 401 | 402 | // remote branches 403 | remote, err := repo.Remote("origin") 404 | if err != nil { 405 | return nil, err 406 | } 407 | refList, err := remote.List(&git.ListOptions{}) 408 | if err != nil { 409 | return nil, err 410 | } 411 | 412 | refPrefix := "refs/heads/" 413 | for _, ref := range refList { 414 | refName := ref.Name().String() 415 | if strings.HasPrefix(refName, refPrefix) { 416 | branches = append(branches, "remotes/origin/"+ref.Name().Short()) 417 | } 418 | } 419 | 420 | return branches, nil 421 | } 422 | 423 | func (g *GitBuilder) isValidTable(tableName string) error { 424 | if _, isOk := g.possibleTables[tableName]; !isOk { 425 | return throwRuntimeError(fmt.Sprintf("Table '%s' not found", tableName), 0) 426 | } 427 | 428 | return nil 429 | } 430 | 431 | func (g *GitBuilder) UseFieldFromTable(field string, tableName string) error { 432 | err := g.isValidTable(tableName) 433 | if err != nil { 434 | return err 435 | } 436 | 437 | if field == "*" { 438 | return nil 439 | } 440 | 441 | table := g.possibleTables[tableName] 442 | for _, t := range table { 443 | if t == field { 444 | return nil 445 | } 446 | } 447 | 448 | return throwRuntimeError(fmt.Sprintf("Table '%s' has not field '%s'", tableName, field), 0) 449 | } 450 | -------------------------------------------------------------------------------- /runtime/runtime_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/cloudson/gitql/parser" 8 | "github.com/cloudson/gitql/semantical" 9 | ) 10 | 11 | func TestErrorWithInvalidTables(t *testing.T) { 12 | t.Skip("Skipping in favor of integration tests") 13 | invalidTables := []string{ 14 | "cloudson", 15 | "blah", 16 | } 17 | 18 | var path string 19 | path, _ = filepath.Abs("../") 20 | gb := GetGitBuilder(&path) 21 | for _, tableName := range invalidTables { 22 | err := gb.WithTable(tableName, tableName) 23 | if err == nil { 24 | t.Errorf("Table '%s' should throws an error", tableName) 25 | } 26 | } 27 | } 28 | 29 | func TestTablesWithoutAlias(t *testing.T) { 30 | t.Skip("Skipping in favor of integration tests") 31 | tables := []string{ 32 | "commits", 33 | "tags", 34 | } 35 | 36 | var path string 37 | path, _ = filepath.Abs("../") 38 | gb := GetGitBuilder(&path) 39 | for _, tableName := range tables { 40 | err := gb.WithTable(tableName, "") 41 | if err != nil { 42 | t.Errorf(err.Error()) 43 | } 44 | } 45 | } 46 | 47 | func TestNotFoundFieldsFromTable(t *testing.T) { 48 | t.Skip("Skipping in favor of integration tests") 49 | metadata := [][]string{ 50 | {"commits", "hashas"}, 51 | {"tags", "blah"}, 52 | {"refs", ""}, 53 | } 54 | 55 | var path string 56 | path, _ = filepath.Abs("../") 57 | gb := GetGitBuilder(&path) 58 | for _, tableMetada := range metadata { 59 | err := gb.UseFieldFromTable(tableMetada[1], tableMetada[0]) 60 | if err == nil { 61 | t.Errorf("Table '%s' should has not field '%s'", tableMetada[0], tableMetada[1]) 62 | } 63 | } 64 | } 65 | 66 | func TestAccepNoIdInLeftValueAtInOperator(t *testing.T) { 67 | t.Skip("Skipping in favor of integration tests") 68 | } 69 | 70 | func TestFoundFieldsFromTable(t *testing.T) { 71 | t.Skip("Skipping in favor of integration tests") 72 | metadata := [][]string{ 73 | {"commits", "*"}, 74 | {"branches", "hash"}, 75 | {"tags", "hash"}, 76 | } 77 | 78 | var path string 79 | path, _ = filepath.Abs("../") 80 | gb := GetGitBuilder(&path) 81 | for _, tableMetada := range metadata { 82 | err := gb.UseFieldFromTable(tableMetada[1], tableMetada[0]) 83 | if err != nil { 84 | t.Errorf(err.Error()) 85 | } 86 | } 87 | } 88 | 89 | func TestCanConvertToTypeFormats(t *testing.T) { 90 | t.Skip("Skipping in favor of integration tests") 91 | folder, errFile := filepath.Abs("../") 92 | 93 | if errFile != nil { 94 | t.Errorf(errFile.Error()) 95 | } 96 | 97 | query := "select author from commits" 98 | 99 | parser.New(query) 100 | ast, errGit := parser.AST() 101 | if errGit != nil { 102 | t.Errorf(errGit.Error()) 103 | } 104 | ast.Path = &folder 105 | errGit = semantical.Analysis(ast) 106 | if errGit != nil { 107 | t.Errorf(errGit.Error()) 108 | } 109 | 110 | typeFormat := "json" 111 | RunSelect(ast, &typeFormat) 112 | } 113 | 114 | func TestNotFoundCommitWithInStatementAndSorting(t *testing.T) { 115 | t.Skip("Skipping in favor of integration tests") 116 | folder, errFile := filepath.Abs("../") 117 | 118 | if errFile != nil { 119 | t.Errorf(errFile.Error()) 120 | } 121 | 122 | query := "select author from commits where 'thisisnotfound' in hash order by date desc" 123 | 124 | parser.New(query) 125 | ast, errGit := parser.AST() 126 | if errGit != nil { 127 | t.Errorf(errGit.Error()) 128 | } 129 | ast.Path = &folder 130 | errGit = semantical.Analysis(ast) 131 | if errGit != nil { 132 | t.Errorf(errGit.Error()) 133 | } 134 | 135 | typeFormat := "table" 136 | if errGit = RunSelect(ast, &typeFormat); errGit != nil { 137 | t.Errorf(errGit.Error()) 138 | } 139 | } 140 | 141 | func TestFoundCommitsWithSevenInHash(t *testing.T) { 142 | t.Skip("Skipping in favor of integration tests") 143 | folder, errFile := filepath.Abs("../") 144 | 145 | if errFile != nil { 146 | t.Errorf(errFile.Error()) 147 | } 148 | 149 | query := "select count(*) from commits where '7' in hash order by date desc limit 1000" 150 | 151 | parser.New(query) 152 | ast, errGit := parser.AST() 153 | if errGit != nil { 154 | t.Errorf(errGit.Error()) 155 | } 156 | ast.Path = &folder 157 | errGit = semantical.Analysis(ast) 158 | if errGit != nil { 159 | t.Errorf(errGit.Error()) 160 | } 161 | 162 | typeFormat := "table" 163 | if errGit = RunSelect(ast, &typeFormat); errGit != nil { 164 | t.Errorf(errGit.Error()) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /runtime/visitor.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/cloudson/gitql/parser" 8 | "github.com/cloudson/gitql/utilities" 9 | ) 10 | 11 | func (v *RuntimeVisitor) Visit(n *parser.NodeProgram) error { 12 | return v.VisitSelect(n.Child.(*parser.NodeSelect)) 13 | } 14 | 15 | func (v *RuntimeVisitor) VisitSelect(n *parser.NodeSelect) error { 16 | if builder.isProxyTable(n.Tables[0]) { 17 | proxyTableName := n.Tables[0] 18 | // refactor tree 19 | proxy := builder.proxyTables[proxyTableName] 20 | if n.Count { 21 | // do nothing 22 | } else if !n.WildCard { 23 | err := testAllFieldsFromTable(n.Fields, proxyTableName) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | } else { 29 | n.Fields = builder.possibleTables[proxyTableName] 30 | n.WildCard = false 31 | } 32 | 33 | err := testAllFieldsInExpr(n.Where, proxyTableName) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | n.Tables[0] = proxy.table 39 | var from, to string 40 | for from, to = range proxy.fields { 41 | break 42 | } 43 | 44 | oldWhere := n.Where 45 | where := new(parser.NodeAnd) 46 | condition := new(parser.NodeEqual) 47 | conditionL := new(parser.NodeId) 48 | conditionL.SetValue(from) 49 | conditionR := new(parser.NodeLiteral) 50 | conditionR.SetValue(to) 51 | condition.SetLeftValue(conditionL) 52 | condition.SetRightValue(conditionR) 53 | 54 | where.SetLeftValue(condition) 55 | where.SetRightValue(oldWhere) 56 | 57 | n.Where = where 58 | } 59 | 60 | table := n.Tables[0] 61 | 62 | var err error 63 | err = builder.WithTable(table, table) 64 | if err != nil { 65 | return err 66 | } 67 | if n.Count { 68 | n.Fields = []string{COUNT_FIELD_NAME} 69 | } else { 70 | err = testAllFieldsFromTable(n.Fields, table) 71 | } 72 | return err 73 | // Why not visit expression right now ? 74 | // Because we will, at first, discover the current object 75 | } 76 | 77 | func testAllFieldsInExpr(expr parser.NodeExpr, tableName string) error { 78 | var err error 79 | if expr != nil { 80 | switch expr.(type) { 81 | case *parser.NodeAnd, *parser.NodeOr: 82 | err = testAllFieldsInExpr(expr.LeftValue(), tableName) 83 | if err == nil { 84 | return testAllFieldsInExpr(expr.RightValue(), tableName) 85 | } 86 | case *parser.NodeEqual, *parser.NodeNotEqual, *parser.NodeGreater, *parser.NodeSmaller, *parser.NodeIn: 87 | return testAllFieldsInExpr(expr.LeftValue(), tableName) 88 | case *parser.NodeId: 89 | field := expr.(*parser.NodeId).Value() 90 | if !utilities.IsFieldPresentInArray(builder.possibleTables[tableName], field) { 91 | return fmt.Errorf("Table '%s' has not field '%s'", tableName, field) 92 | } 93 | } 94 | } 95 | return err 96 | } 97 | func testAllFieldsFromTable(fields []string, table string) error { 98 | for _, f := range fields { 99 | err := builder.UseFieldFromTable(f, table) 100 | if err != nil { 101 | return err 102 | } 103 | } 104 | return nil 105 | } 106 | 107 | func (v *RuntimeVisitor) VisitExpr(n parser.NodeExpr) error { 108 | switch reflect.TypeOf(n) { 109 | case reflect.TypeOf(new(parser.NodeEqual)): 110 | g := n.(*parser.NodeEqual) 111 | return v.VisitEqual(g) 112 | case reflect.TypeOf(new(parser.NodeGreater)): 113 | g := n.(*parser.NodeGreater) 114 | return v.VisitGreater(g) 115 | case reflect.TypeOf(new(parser.NodeSmaller)): 116 | g := n.(*parser.NodeSmaller) 117 | return v.VisitSmaller(g) 118 | case reflect.TypeOf(new(parser.NodeOr)): 119 | g := n.(*parser.NodeOr) 120 | return v.VisitOr(g) 121 | case reflect.TypeOf(new(parser.NodeAnd)): 122 | g := n.(*parser.NodeAnd) 123 | return v.VisitAnd(g) 124 | case reflect.TypeOf(new(parser.NodeIn)): 125 | g := n.(*parser.NodeIn) 126 | return v.VisitIn(g) 127 | case reflect.TypeOf(new(parser.NodeLike)): 128 | g := n.(*parser.NodeLike) 129 | return v.VisitLike(g) 130 | case reflect.TypeOf(new(parser.NodeNotEqual)): 131 | g := n.(*parser.NodeNotEqual) 132 | return v.VisitNotEqual(g) 133 | } 134 | return nil 135 | } 136 | 137 | func (v *RuntimeVisitor) VisitEqual(n *parser.NodeEqual) error { 138 | lvalue := n.LeftValue().(*parser.NodeId).Value() 139 | rvalue := n.RightValue().(*parser.NodeLiteral).Value() 140 | boolRegister = n.Assertion(metadata(lvalue), rvalue) 141 | return nil 142 | } 143 | 144 | func (v *RuntimeVisitor) VisitLike(n *parser.NodeLike) error { 145 | lvalue := n.LeftValue().(*parser.NodeId).Value() 146 | rvalue := n.RightValue().(*parser.NodeLiteral).Value() 147 | boolRegister = n.Assertion(metadata(lvalue), rvalue) 148 | return nil 149 | } 150 | 151 | func (v *RuntimeVisitor) VisitNotEqual(n *parser.NodeNotEqual) error { 152 | lvalue := n.LeftValue().(*parser.NodeId).Value() 153 | rvalue := n.RightValue().(*parser.NodeLiteral).Value() 154 | boolRegister = n.Assertion(metadata(lvalue), rvalue) 155 | return nil 156 | } 157 | 158 | func (v *RuntimeVisitor) VisitGreater(n *parser.NodeGreater) error { 159 | lvalue := n.LeftValue().(*parser.NodeId).Value() 160 | lvalue = metadata(lvalue) 161 | rvalue := n.RightValue().(*parser.NodeLiteral).Value() 162 | boolRegister = n.Assertion(lvalue, rvalue) 163 | return nil 164 | } 165 | 166 | func (v *RuntimeVisitor) VisitSmaller(n *parser.NodeSmaller) error { 167 | lvalue := n.LeftValue().(*parser.NodeId).Value() 168 | lvalue = metadata(lvalue) 169 | rvalue := n.RightValue().(*parser.NodeLiteral).Value() 170 | boolRegister = n.Assertion(lvalue, rvalue) 171 | return nil 172 | } 173 | 174 | func (v *RuntimeVisitor) VisitOr(n *parser.NodeOr) error { 175 | v.VisitExpr(n.LeftValue()) 176 | boolLeft := boolRegister 177 | v.VisitExpr(n.RightValue()) 178 | boolRight := boolRegister 179 | boolRegister = boolLeft || boolRight 180 | return nil 181 | } 182 | 183 | func (v *RuntimeVisitor) VisitAnd(n *parser.NodeAnd) error { 184 | v.VisitExpr(n.LeftValue()) 185 | boolLeft := boolRegister 186 | v.VisitExpr(n.RightValue()) 187 | boolRight := boolRegister 188 | 189 | boolRegister = boolLeft && boolRight 190 | return nil 191 | } 192 | 193 | func (v *RuntimeVisitor) VisitIn(n *parser.NodeIn) error { 194 | lvalue := n.LeftValue().(*parser.NodeLiteral).Value() 195 | rvalue := n.RightValue().(*parser.NodeId).Value() 196 | boolRegister = n.Assertion(lvalue, metadata(rvalue)) 197 | 198 | return nil 199 | } 200 | 201 | func (v *RuntimeVisitor) Builder() *GitBuilder { 202 | return nil 203 | } 204 | -------------------------------------------------------------------------------- /runtime/visitor_test.go: -------------------------------------------------------------------------------- 1 | package runtime 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | 7 | "github.com/cloudson/gitql/parser" 8 | "github.com/cloudson/gitql/semantical" 9 | ) 10 | 11 | func TestTestAllFieldsInExprBranches(t *testing.T) { 12 | query := "select * from branches where name = 'something' and somthing > 'name'" 13 | err := parseAndVisitQuery(query, "../", t) 14 | if err == nil { 15 | t.Error("Expected error, received none") 16 | } 17 | } 18 | 19 | func TestTestAllFieldsInExprBranchesWithCount(t *testing.T) { 20 | query := "select count(*) from branches where name = 'something' and somthing > 'name'" 21 | err := parseAndVisitQuery(query, "../", t) 22 | if err == nil { 23 | t.Error("Expected error, received none") 24 | } 25 | } 26 | 27 | func TestTestAllFieldsInExprRefs(t *testing.T) { 28 | query := "select * from refs where name = 'something' or type = 'asdfasdfsd'" 29 | err := parseAndVisitQuery(query, "../", t) 30 | if err != nil { 31 | t.Errorf("Unexpedted error %s", err) 32 | } 33 | } 34 | 35 | func TestTestAllFieldsInExprTags(t *testing.T) { 36 | query := "select * from tags where type = 'blah'" 37 | err := parseAndVisitQuery(query, "../", t) 38 | if err == nil { 39 | t.Errorf("Unexpedted error %s", err) 40 | } 41 | } 42 | 43 | func parseAndVisitQuery(query, dir string, t *testing.T) error { 44 | parser.New(query) 45 | ast, errGit := parser.AST() 46 | failTestIfError(errGit, t) 47 | 48 | folder, errFile := filepath.Abs(dir) 49 | failTestIfError(errFile, t) 50 | ast.Path = &folder 51 | errGit = semantical.Analysis(ast) 52 | failTestIfError(errGit, t) 53 | 54 | builder = GetGitBuilder(ast.Path) 55 | visitor := new(RuntimeVisitor) 56 | err := visitor.Visit(ast) 57 | return err 58 | } 59 | -------------------------------------------------------------------------------- /semantical/semantical.go: -------------------------------------------------------------------------------- 1 | package semantical 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cloudson/gitql/lexical" 7 | "github.com/cloudson/gitql/parser" 8 | ) 9 | 10 | func Analysis(ast *parser.NodeProgram) error { 11 | semantic := new(SemanticalVisitor) 12 | 13 | return semantic.Visit(ast) 14 | } 15 | 16 | type semanticalError struct { 17 | err string 18 | errNo uint8 19 | } 20 | 21 | func throwSemanticalError(err string) error { 22 | end := new(semanticalError) 23 | end.err = err 24 | 25 | return end 26 | } 27 | 28 | func (e *semanticalError) Error() string { 29 | return e.err 30 | } 31 | 32 | func (v *SemanticalVisitor) Visit(n *parser.NodeProgram) error { 33 | return v.VisitSelect(n.Child.(*parser.NodeSelect)) 34 | } 35 | 36 | func (v *SemanticalVisitor) VisitSelect(n *parser.NodeSelect) error { 37 | 38 | fields := n.Fields 39 | fieldsCount := make(map[string]bool) 40 | for _, field := range fields { 41 | if fieldsCount[field] { 42 | return throwSemanticalError(fmt.Sprintf("Field '%s' found many times", field)) 43 | } 44 | 45 | fieldsCount[field] = true 46 | } 47 | 48 | err := v.VisitExpr(n.Where) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if 0 == n.Limit { 54 | return throwSemanticalError("Limit should be greater than zero") 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (v *SemanticalVisitor) VisitExpr(n parser.NodeExpr) error { 61 | if n == nil { 62 | return nil 63 | } 64 | 65 | switch n.Operator() { 66 | case lexical.T_GREATER: 67 | g := n.(*parser.NodeGreater) 68 | return v.VisitGreater(g) 69 | case lexical.T_SMALLER: 70 | g := n.(*parser.NodeSmaller) 71 | return v.VisitSmaller(g) 72 | case lexical.T_IN: 73 | g := n.(*parser.NodeIn) 74 | return v.VisitIn(g) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func (v *SemanticalVisitor) VisitGreater(n *parser.NodeGreater) error { 81 | rVal := n.RightValue() 82 | if !shouldBeNumericOrDate(rVal) { 83 | return throwSemanticalError("RValue in Greater should be numeric or a date") 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (v *SemanticalVisitor) VisitSmaller(n *parser.NodeSmaller) error { 90 | rVal := n.RightValue() 91 | if !shouldBeNumericOrDate(rVal) { 92 | return throwSemanticalError("RValue in Smaller should be numeric or a date") 93 | } 94 | 95 | return nil 96 | } 97 | 98 | func (v *SemanticalVisitor) VisitIn(n *parser.NodeIn) error { 99 | lval := n.LeftValue() 100 | if lval.Operator() != lexical.T_LITERAL { 101 | return throwSemanticalError("LValue at In operator shoud be a literal") 102 | } 103 | 104 | rval := n.RightValue() 105 | if rval.Operator() != lexical.T_ID { 106 | return throwSemanticalError("RValue at In operator shoud be a Identifier") 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func shouldBeNumericOrDate(val parser.NodeExpr) bool { 113 | // if reflect.TypeOf(val) == reflect.TypeOf(new(parser.NodeNumber)) { 114 | if val.Operator() == lexical.T_NUMERIC { 115 | return true 116 | } 117 | 118 | if val.Operator() == lexical.T_LITERAL { 119 | date := parser.ExtractDate(val.(*parser.NodeLiteral).Value()) 120 | if date != nil { 121 | return true 122 | } 123 | } 124 | 125 | return false 126 | } 127 | -------------------------------------------------------------------------------- /semantical/semantical_test.go: -------------------------------------------------------------------------------- 1 | package semantical 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cloudson/gitql/parser" 7 | ) 8 | 9 | func TestInvalidZeroLimit(t *testing.T) { 10 | parser.New("select * from commits limit 0") 11 | ast, parserErr := parser.AST() 12 | if parserErr != nil { 13 | t.Fatalf(parserErr.Error()) 14 | } 15 | 16 | err := Analysis(ast) 17 | if err == nil { 18 | t.Fatalf("Should not accept limit zero") 19 | } 20 | } 21 | 22 | func TestValidNullLimit(t *testing.T) { 23 | parser.New("select * from commits") 24 | ast, parserErr := parser.AST() 25 | if parserErr != nil { 26 | t.Fatalf(parserErr.Error()) 27 | } 28 | 29 | err := Analysis(ast) 30 | if err != nil { 31 | t.Fatalf(err.Error()) 32 | } 33 | } 34 | 35 | func TestValiDistinct(t *testing.T) { 36 | parser.New("select distinct author from commits") 37 | ast, parserErr := parser.AST() 38 | if parserErr != nil { 39 | t.Fatalf(parserErr.Error()) 40 | } 41 | 42 | err := Analysis(ast) 43 | if err != nil { 44 | t.Fatalf(err.Error()) 45 | } 46 | } 47 | 48 | func TestInvaliMultipleDistinct(t *testing.T) { 49 | parser.New("select distinct author, distinct date from commits") 50 | _, parserErr := parser.AST() 51 | if parserErr == nil { 52 | t.Fatalf("Expected a parsing error on double distinct") 53 | } 54 | } 55 | 56 | func TestChooseRepetitiveFields(t *testing.T) { 57 | parser.New("select name, created_at, name from commits") 58 | ast, parserErr := parser.AST() 59 | if parserErr != nil { 60 | t.Fatalf(parserErr.Error()) 61 | } 62 | 63 | err := Analysis(ast) 64 | if err == nil { 65 | t.Fatalf("Shoud avoid repetitive fields") 66 | } 67 | } 68 | 69 | func TestConstantLValue(t *testing.T) { 70 | parser.New("select name from commits where 'name' = 'name' ") 71 | ast, parserErr := parser.AST() 72 | if parserErr != nil { 73 | t.Fatalf(parserErr.Error()) 74 | } 75 | 76 | err := Analysis(ast) 77 | if err != nil { 78 | t.Fatalf(err.Error()) 79 | } 80 | } 81 | 82 | func TestGreaterWithNoNumeric(t *testing.T) { 83 | parser.New("select name from commits where date > 'name'") 84 | ast, parserErr := parser.AST() 85 | if parserErr != nil { 86 | t.Fatalf(parserErr.Error()) 87 | } 88 | 89 | err := Analysis(ast) 90 | if err == nil { 91 | t.Fatalf("Shoud avoid greater with no numeric") 92 | } 93 | } 94 | 95 | func TestSmallerWithInvalidConstant(t *testing.T) { 96 | parser.New("select name from commits where date <= 'name'") 97 | ast, parserErr := parser.AST() 98 | if parserErr != nil { 99 | t.Fatalf(parserErr.Error()) 100 | } 101 | 102 | err := Analysis(ast) 103 | if err == nil { 104 | t.Fatalf("Shoud avoid smaller with no numeric") 105 | } 106 | } 107 | 108 | func TestSmallerWithDate(t *testing.T) { 109 | parser.New("select name from commits where date > '2013-03-14 00:00:00'") 110 | ast, parserErr := parser.AST() 111 | if parserErr != nil { 112 | t.Fatalf(parserErr.Error()) 113 | } 114 | 115 | err := Analysis(ast) 116 | if err != nil { 117 | t.Fatalf(err.Error()) 118 | } 119 | } 120 | 121 | func TestSmallerWithDateWithoutTime(t *testing.T) { 122 | parser.New("select count(*) from commits where date > '2013-03-14'") 123 | ast, parserErr := parser.AST() 124 | if parserErr != nil { 125 | t.Fatalf(parserErr.Error()) 126 | } 127 | 128 | err := Analysis(ast) 129 | if err != nil { 130 | t.Fatalf(err.Error()) 131 | } 132 | } 133 | 134 | func TestRepeatedDistinct(t *testing.T) { 135 | parser.New("select distinct distinct author from commits") 136 | _, parserErr := parser.AST() 137 | if parserErr == nil { 138 | t.Fatalf(parserErr.Error()) 139 | } 140 | } 141 | 142 | // You should not test stupid things like "c" in "cloudson" or 1 = 1 ¬¬ 143 | func TestInUsingNotLiteralLeft(t *testing.T) { 144 | parser.New("select * from commits where 'c' in 'cloudson'") 145 | ast, parserErr := parser.AST() 146 | if parserErr != nil { 147 | t.Fatalf(parserErr.Error()) 148 | } 149 | 150 | err := Analysis(ast) 151 | if err == nil { 152 | t.Fatalf("Should trow error with invalid in ") 153 | } 154 | } 155 | 156 | func TestInUsingNotIdRight(t *testing.T) { 157 | parser.New("select * from commits where 'c' in 'cc' ") 158 | ast, parserErr := parser.AST() 159 | if parserErr != nil { 160 | t.Fatalf(parserErr.Error()) 161 | } 162 | 163 | err := Analysis(ast) 164 | if err == nil { 165 | t.Fatalf("Should trow error with invalid in ") 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /semantical/visitor.go: -------------------------------------------------------------------------------- 1 | package semantical 2 | 3 | import ( 4 | "github.com/cloudson/gitql/parser" 5 | ) 6 | 7 | type Visitor interface { 8 | Visit(*parser.NodeProgram) error 9 | VisitSelect(*parser.NodeSelect) error 10 | VisitExpr(*parser.NodeExpr) error 11 | VisitGreater(*parser.NodeGreater) error 12 | VisitSmaller(*parser.NodeSmaller) error 13 | VisitIn(*parser.NodeSmaller) error 14 | VisitEqual(*parser.NodeSmaller) error 15 | VisitNotEqual(*parser.NodeSmaller) error 16 | } 17 | 18 | type SemanticalVisitor struct { 19 | Visitor 20 | } 21 | -------------------------------------------------------------------------------- /tables.md: -------------------------------------------------------------------------------- 1 | Gitql [![Build Status](https://travis-ci.org/cloudson/gitql.png)](https://travis-ci.org/cloudson/gitql) 2 | =============== 3 | 4 | ## Tables 5 | 6 | | commits | 7 | | ---------| 8 | | author | 9 | | author_email | 10 | | committer | 11 | | committer_email | 12 | | hash | 13 | | date | 14 | | message | 15 | | full_message | 16 | 17 | | tags | 18 | | ---------| 19 | | name | 20 | | full_name | 21 | | hash | 22 | 23 | | branches | 24 | | ---------| 25 | | name | 26 | | full_name | 27 | | hash | 28 | 29 | | Refs | 30 | | ---------| 31 | | name | 32 | | full_name | 33 | | type | 34 | | hash | 35 | -------------------------------------------------------------------------------- /test/options.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # test that will fail outside of pipeline because 4 | # it requires an override of the version.txt 5 | # you can check how it's done on github actions files. 6 | @test "Check version with -v" { 7 | result="$(./gitql -v)" 8 | [ "$result" != "Gitql latest\n" ] 9 | } 10 | 11 | @test "Check version" { 12 | result="$(./gitql version)" 13 | [ "$result" != "Gitql latest\n" ] 14 | } 15 | 16 | @test "Check table commits on -s" { 17 | result="$(./gitql -s | grep commits)" 18 | [ "$result" == "commits" ] 19 | } 20 | 21 | @test "Check table refs on -s" { 22 | result="$(./gitql -s | grep refs)" 23 | [ "$result" == "refs" ] 24 | } 25 | 26 | @test "Check table tags on -s" { 27 | result="$(./gitql -s | grep tags)" 28 | [ "$result" == "tags" ] 29 | } 30 | 31 | @test "Check table branches on -s" { 32 | result="$(./gitql -s | grep branches)" 33 | [ "$result" == "branches" ] 34 | } 35 | 36 | @test "Check exit code for help" { 37 | result="$(./gitql)" 38 | [ "$?" == "0" ] 39 | } -------------------------------------------------------------------------------- /test/select.bats: -------------------------------------------------------------------------------- 1 | setup() { 2 | load 'test_helper/bats-support/load' 3 | load 'test_helper/bats-assert/load' 4 | } 5 | 6 | @test "Check exit code for select" { 7 | run ./gitql 'select * from commits' 8 | [ "$status" -eq 0 ] 9 | } 10 | 11 | @test "Check like for select" { 12 | run ./gitql -f json 'select message from commits where date > "2020-10-04" and date < "2020-10-06" and message like "update"' 13 | assert_output '[{"message":"update Dockerfile (#97)"}]' 14 | } 15 | 16 | @test "Check wrong query with double quote" { 17 | run ./gitql -f json "select author, message, date from commits where author like "Oliveira"" 18 | assert_output 'Error: Expected T_LITERAL and found T_EOF' 19 | } 20 | 21 | @test "Check not like for select" { 22 | run ./gitql -f json 'select message from commits where date > "2019-10-01" and date < "2019-11-01" and message not like "update"' 23 | assert_output '[{"message":"Update How-To video with new prompts (#89)"},{"message":"Prepare v2.0.0 (#88)"},{"message":"Add support to static binaries (windows/linux amd64) (#87)"},{"message":"Add install libgit script (#86)"},{"message":"Add support to dynamic compile for mac (#85)"},{"message":"Add support to release gitql as a static file (#84)"}]' 24 | } 25 | 26 | @test "Check in for select" { 27 | run ./gitql -f json 'select distinct author from commits where "Tadeu" in author and date < "2021-01-01"' 28 | assert_output '[{"author":"Tadeu Zagallo"}]' 29 | } 30 | 31 | @test "Check count for select" { 32 | run ./gitql -f json 'select count(*) from commits where date > "2019-10-09" and date < "2019-10-17"' 33 | assert_output '[{"count":"2"}]' 34 | } 35 | 36 | @test "Select distinct should works" { 37 | run ./gitql -f json 'select distinct author from commits where date > "2019-10-01" and date < "2019-11-01" order by author asc' 38 | assert_output '[{"author":"Arumugam Jeganathan"},{"author":"Claudson Oliveira"}]' 39 | } 40 | 41 | @test "Select count should works" { 42 | run ./gitql -f json 'select count(*) from commits where date < "2018-01-05"' 43 | assert_output '[{"count":"191"}]' 44 | } 45 | 46 | @test "Select should works with order and limit" { 47 | run ./gitql -f json 'select date, message from commits where date < "2020-10-01" order by date desc limit 3' 48 | assert_output '[{"date":"2020-08-05 22:12:16","message":"Update README.md"},{"date":"2020-08-05 22:11:43","message":"Update README.md"},{"date":"2019-11-11 21:35:16","message":"Run tests on pull requests (#91)"}]' 49 | } 50 | 51 | # bugs to be fixed 52 | 53 | @test "Check incorrect usage of in for select" { 54 | skip "Should fail gracefully when using in the wrong way" 55 | run ./gitql -f json 'select distinct author from commits where author in "Tadeu" and date < "2021-01-01"' 56 | assert_output 'Unexpected T_IN after T_ID' 57 | } 58 | 59 | @test "Check incorrect json output of select" { 60 | skip "Should not return any other field than message" 61 | run ./gitql -f json 'select message from commits where date < "2021-05-27" order by date desc limit 3' 62 | assert_output '[{"message":"Add smoke test about count"},{"message":"Smoke test on select discinct"},{"message":"Remove bats for windows"}]' 63 | } -------------------------------------------------------------------------------- /test/use.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | setup() { 4 | export branch=$(git branch | grep "\*" | rev | cut -d " " -f 1 | rev | tr -d ")") 5 | } 6 | 7 | teardown() { 8 | git checkout $branch &> /dev/null 9 | } 10 | 11 | @test "Check switching to existing branch" { 12 | run ./gitql "use main" 13 | [ "$status" -eq 0 ] 14 | } 15 | 16 | @test "Check switching to nonexistent branch" { 17 | run ./gitql 'use this-is-not-a-branch' 18 | [ "$status" -eq 1 ] 19 | } 20 | -------------------------------------------------------------------------------- /utilities/utilities.go: -------------------------------------------------------------------------------- 1 | package utilities 2 | 3 | // IsFieldPresentInArray checks the array of strings for the given field name. 4 | func IsFieldPresentInArray(arr []string, element string) bool { 5 | for _, fieldInSelect := range arr { 6 | if fieldInSelect == element { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /utilities/utilities_test.go: -------------------------------------------------------------------------------- 1 | package utilities 2 | 3 | import "testing" 4 | 5 | func TestIsFieldPresentInArray(t *testing.T) { 6 | testArray := []string{"first", "second", "third", "fourth"} 7 | 8 | if IsFieldPresentInArray(testArray, "fifth") { 9 | t.Error("Fifth should not be in the test Array") 10 | } 11 | 12 | if !IsFieldPresentInArray(testArray, "first") { 13 | t.Error("First should bin in the testArray") 14 | } 15 | 16 | if IsFieldPresentInArray(testArray, "second") { 17 | return 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | latest --------------------------------------------------------------------------------