diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..78e04ee --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,51 @@ +name: build + +on: + push: + branches: + - master + paths-ignore: + - '**.md' + - '.gitignore' + pull_request: + paths-ignore: + - '**.md' + - '.gitignore' + +jobs: + build: + name: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: + - linux + - darwin + - windows + arch: + - amd64 + - arm + exclude: + - os: darwin + arch: arm + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 1 + - name: setup-go 1.14 + uses: actions/setup-go@v1 + with: + go-version: 1.14 + - name: build + run: | + go build + env: + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + - name: test + if: matrix.os == 'linux' && matrix.arch == 'amd64' + run: | + set -e + ./nebula-console2.0 -e 'exit' + ./nebula-console2.0 -f demo.nGQL diff --git a/.gitignore b/.gitignore index 66fd13c..6cf663e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +nebula-console diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..386d40b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.14.2 as builder + +COPY . /usr/src + +RUN cd /usr/src && go build + +FROM centos:7 + +COPY --from=builder /usr/src/nebula-console /usr/bin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..847fc91 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Nebula Graph Console + +The console for Nebula Graph 2.0 + +# Build + +Install golang by https://golang.org/doc/install, then try `go build` + +# Usage + +Check options for `./nebula-console -h`, try `./nebula-console` in interactive mode directly. +And try `./nebula-console -e 'exit'` for the direct script mode. +And try `./nebula-console -f demo.nGQL` for the script file mode. + +# Feature + +- Interactive and non-interactive +- History +- Autocompletion +- Multiple OS and arch supported (linux/amd64 recommend) + +# TODO + +- CI/CD +- package to RPM/DEB/DOCKER +- batch process to reduce memory consumption and speed up IO diff --git a/ci/scripts/upload-github-release-asset.sh b/ci/scripts/upload-github-release-asset.sh new file mode 100755 index 0000000..cb5b82d --- /dev/null +++ b/ci/scripts/upload-github-release-asset.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# +# This script accepts the following 4 parameters to upload a release asset using the GitHub API v3. +# +# - github_token +# - repo +# - tag +# - filepath +# +# Example: +# +# upload-github-release-asset.sh github_token=TOKEN repo=vesoft-inc/nebula tag=v0.1.0 filepath=./asset.zip + +set -e + +for op in $@; do + eval "$op" +done + +GH_RELEASE="https://api.github.com/repos/$repo/releases/tags/$tag" + +upload_url=$(curl -s --request GET --url $GH_RELEASE | grep -oP '(?<="upload_url": ")[^"]*' | cut -d'{' -f1) + +content_type=$(file -b --mime-type $filepath) + +filename=$(basename "$filepath") + +echo "Uploading asset... " + +curl --silent \ + --request POST \ + --url "$upload_url?name=$filename" \ + --header "authorization: Bearer $github_token" \ + --header "content-type: $content_type" \ + --data-binary @"$filepath" diff --git a/ci/scripts/upload-oss.sh b/ci/scripts/upload-oss.sh new file mode 100755 index 0000000..2c88be1 --- /dev/null +++ b/ci/scripts/upload-oss.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# This script upload package to oss. +# +# - OSS_ENDPOINT +# - OSS_ID +# - OSS_SECRET +# - filepath +# - tag +# - nightly +# +# Example: +# +# upload-oss.sh OSS_ENDPOINT=xxx OSS_ID=xxx OSS_SECRET=xxx tag=v0.1.0 filepath=xxx +# upload-oss.sh OSS_ENDPOINT=xxx OSS_ID=xxx OSS_SECRET=xxx filepath=xxx nightly=true + +set -e + +for op in $@; do + eval "$op" +done + +if [[ $nightly != "" ]]; then + OSS_SUBDIR=`date +%Y%m%d` + OSS_URL="oss://nebula-graph/build-deb"/nightly/${OSS_SUBDIR} +else + OSS_SUBDIR=`echo $tag |sed 's/^v//'` + OSS_URL="oss://nebula-graph/build-deb"/${OSS_SUBDIR} +fi + +echo "Uploading oss... " +ossutil64 -e ${OSS_ENDPOINT} -i ${OSS_ID} -k ${OSS_SECRET} -f cp ${filepath} ${OSS_URL}/$(basename ${filepath}) diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..a7faf7e --- /dev/null +++ b/cli.go @@ -0,0 +1,220 @@ +/* Copyright (c) 2020 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License, + * attached with Common Clause Condition 1.0, found in the LICENSES directory. + */ + +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "path" + + readline "github.com/vesoft-inc/readline" +) + +const ttyColorPrefix = "\033[" +const ttyColorSuffix = "m" +const ttyColorRed = "31" +const ttyColorBold = "1" +const ttyColorReset = "0" + +var completer = readline.NewPrefixCompleter( + // show + readline.PcItem("SHOW", + readline.PcItem("HOSTS"), + readline.PcItem("SPACES"), + readline.PcItem("PARTS"), + readline.PcItem("TAGS"), + readline.PcItem("EDGES"), + readline.PcItem("USERS"), + readline.PcItem("ROLES"), + readline.PcItem("USER"), + readline.PcItem("CONFIGS"), + ), + + // describe + readline.PcItem("DESCRIBE", + readline.PcItem("TAG"), + readline.PcItem("EDGE"), + readline.PcItem("SPACE"), + ), + readline.PcItem("DESC", + readline.PcItem("TAG"), + readline.PcItem("EDGE"), + readline.PcItem("SPACE"), + ), + // get configs + readline.PcItem("GET", + readline.PcItem("CONFIGS"), + ), + // create + readline.PcItem("CREATE", + readline.PcItem("SPACE"), + readline.PcItem("TAG"), + readline.PcItem("EDGE"), + readline.PcItem("USER"), + ), + // drop + readline.PcItem("DROP", + readline.PcItem("SPACE"), + readline.PcItem("TAG"), + readline.PcItem("EDGE"), + readline.PcItem("USER"), + ), + // alter + readline.PcItem("ALTER", + readline.PcItem("USER"), + readline.PcItem("TAG"), + readline.PcItem("EDGE"), + ), + + // insert + readline.PcItem("INSERT", + readline.PcItem("VERTEX"), + readline.PcItem("EDGE"), + ), + // update + readline.PcItem("UPDATE", + readline.PcItem("CONFIGS"), + readline.PcItem("VERTEX"), + readline.PcItem("EDGE"), + ), + // upsert + readline.PcItem("UPSERT", + readline.PcItem("VERTEX"), + readline.PcItem("EDGE"), + ), + // delete + readline.PcItem("DELETE", + readline.PcItem("VERTEX"), + readline.PcItem("EDGE"), + ), + + // grant + readline.PcItem("GRANT", + readline.PcItem("ROLE"), + ), + // revoke + readline.PcItem("REVOKE", + readline.PcItem("ROLE"), + ), + // change password + readline.PcItem("CHANGE", + readline.PcItem("PASSWORD"), + ), +) + +func promptString(space string, user string, isErr bool, isTTY bool) string { + prompt := "" + // (user@nebula) [(space)] > + if isTTY { + prompt += fmt.Sprintf("%s%s%s", ttyColorPrefix, ttyColorBold, ttyColorSuffix) + } + if isTTY && isErr { + prompt += fmt.Sprintf("%s%s%s", ttyColorPrefix, ttyColorRed, ttyColorSuffix) + } + prompt += fmt.Sprintf("(%s@%s) [(%s)]> ", user, NebulaLabel, space) + if isTTY { + prompt += fmt.Sprintf("%s%s%s", ttyColorPrefix, ttyColorReset, ttyColorSuffix) + } + return prompt +} + +type Cli interface { + ReadLine() ( /*line*/ string /*err*/, error /*exit*/, bool) + Interactive() bool + SetisErr(bool) + SetSpace(string) +} + +// interactive +type iCli struct { + input *readline.Instance + user string + space string + isErr bool + isTTY bool +} + +func NewiCli(home string, user string) *iCli { + r, err := readline.NewEx(&readline.Config{ + // See https://github.com/chzyer/readline/issues/169 + Prompt: nil, + HistoryFile: path.Join(home, ".nebula_history"), + AutoComplete: completer, + InterruptPrompt: "^C", + EOFPrompt: "", + HistorySearchFold: true, + FuncFilterInputRune: nil, + }) + if err != nil { + log.Fatalf("Create readline failed, %s.", err.Error()) + } + isTTY := readline.IsTerminal(int(os.Stdout.Fd())) + icli := &iCli{r, user, "", false, isTTY} + icli.input.SetPrompt(func() []rune { + return []rune(promptString(icli.space, icli.user, icli.isErr, icli.isTTY)) + }) + return icli +} + +func (l *iCli) SetSpace(space string) { + l.space = space +} + +func (l *iCli) SetisErr(isErr bool) { + l.isErr = isErr +} + +func (l iCli) ReadLine() (string, error, bool) { + get, err := l.input.Readline() + if err == io.EOF || err == readline.ErrInterrupt { + // Ending not error + return get, nil, true + } + if err != nil { + return get, err, true + } + return get, err, false +} + +func (l iCli) Interactive() bool { + return true +} + +// non-interactive +type nCli struct { + io *bufio.Reader +} + +func NewnCli(i io.Reader) nCli { + return nCli{bufio.NewReader(i)} +} + +func (l nCli) ReadLine() (string, error, bool) { + s, _, e := l.io.ReadLine() + if e == io.EOF { + return string(s), nil, true + } + if e != nil { + return string(s), e, true + } + return string(s), e, false +} + +func (l nCli) Interactive() bool { + return false +} + +func (l nCli) SetSpace(space string) { + // nothing +} + +func (l nCli) SetisErr(isErr bool) { + // nothing +} diff --git a/demo.nGQL b/demo.nGQL new file mode 100644 index 0000000..a3abe50 --- /dev/null +++ b/demo.nGQL @@ -0,0 +1 @@ +exit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fd96723 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module vesoft-inc/nebula-console + +require ( + github.com/vesoft-inc/nebula-go/v2 v2.0.0-20200722013035-73eebb836e7e + github.com/vesoft-inc/readline v0.0.0-20200707112557-889240450af4 + golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..73948c4 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/facebook/fbthrift v0.0.0-20190922225929-2f9839604e25/go.mod h1:2tncLx5rmw69e5kMBv/yJneERbzrr1yr5fdlnTbu8lU= +github.com/vesoft-inc/nebula-go/v2 v2.0.0-20200722013035-73eebb836e7e h1:CV/gYc4QM4fyfpxfShsxQ8GgG2vu6u1fP9mhtIB17Sw= +github.com/vesoft-inc/nebula-go/v2 v2.0.0-20200722013035-73eebb836e7e/go.mod h1:92I8vrIc2Rk7/oJO+1hTgjNhHiYfnsW2uu3Ofh0ECnI= +github.com/vesoft-inc/readline v0.0.0-20200707112557-889240450af4 h1:DiyiidZ9FBSwOZfslQeIPeCUo7AjJ59bj3UaTQEY7po= +github.com/vesoft-inc/readline v0.0.0-20200707112557-889240450af4/go.mod h1:E6VDTX2OSL0so+MQj23uMHpp24E2gqbl5nVOSviDIkU= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f5abfa1 --- /dev/null +++ b/main.go @@ -0,0 +1,157 @@ +/* Copyright (c) 2020 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License, + * attached with Common Clause Condition 1.0, found in the LICENSES directory. + */ + +package main + +import ( + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" + + ngdb "github.com/vesoft-inc/nebula-go/v2" + graph "github.com/vesoft-inc/nebula-go/v2/nebula/graph" +) + +const NebulaLabel = "Nebula-Console" +const Version = "v2.0.0-alpha" + +func welcome(interactive bool) { + if !interactive { + return + } + fmt.Printf("Welcome to Nebula Graph %s!", Version) + fmt.Println() +} + +func bye(username string, interactive bool) { + if !interactive { + return + } + fmt.Printf("Bye %s!", username) + fmt.Println() +} + +// return , does exit +func clientCmd(query string) bool { + plain := strings.ToLower(strings.TrimSpace(query)) + if plain == "exit" || plain == "quit" { + return true + } + return false +} + +var t = NewTable(2, "=", "-", "|") + +func printResp(resp *graph.ExecutionResponse, duration time.Duration) { + // Error + if resp.GetErrorCode() != graph.ErrorCode_SUCCEEDED { + fmt.Printf("[ERROR (%d)]: %s", resp.GetErrorCode(), resp.GetErrorMsg()) + fmt.Println() + return + } + // Show table + if resp.GetData() != nil { + t.PrintTable(resp.GetData()) + } + // Show time + fmt.Printf("time spent %d/%d us", resp.GetLatencyInUs(), duration /*ns*/ /1000) + fmt.Println() +} + +// Loop the request util fatal or timeout +// We treat one line as one query +// Add line break yourself as `SHOW \HOSTS` +func loop(client *ngdb.GraphClient, c Cli) error { + for true { + line, err, exit := c.ReadLine() + lineString := string(line) + if exit { + return err + } + if len(line) == 0 { + fmt.Println() + continue + } + + // Client side command + if clientCmd(lineString) { + // Quit + return nil + } + + start := time.Now() + resp, err := client.Execute(lineString) + duration := time.Since(start) + if err != nil { + // Exception + log.Fatalf("Execute error, %s", err.Error()) + } + printResp(resp, duration) + fmt.Println(time.Now().Format("2006-01-02 15:04:05")) + c.SetSpace(string(resp.SpaceName)) + c.SetisErr(resp.GetErrorCode() != graph.ErrorCode_SUCCEEDED) + fmt.Println() + } + return nil +} + +func main() { + address := flag.String("address", "127.0.0.1", "The Nebula Graph IP address") + port := flag.Int("port", 3699, "The Nebula Graph Port") + username := flag.String("u", "user", "The Nebula Graph login user name") + password := flag.String("p", "password", "The Nebula Graph login password") + script := flag.String("e", "", "The nGQL directly") + file := flag.String("f", "", "The nGQL script file name") + flag.Parse() + + interactive := *script == "" && *file == "" + + historyHome := os.Getenv("HOME") + if historyHome == "" { + ex, err := os.Executable() + if err != nil { + log.Fatalf("Get executable failed: %s", err.Error()) + } + historyHome = filepath.Dir(ex) // Set to executable folder + } + + client, err := ngdb.NewClient(fmt.Sprintf("%s:%d", *address, *port)) + if err != nil { + log.Fatalf("Fail to create client, address: %s, port: %d, %s", *address, *port, err.Error()) + } + + if err = client.Connect(*username, *password); err != nil { + log.Fatalf("Fail to connect server, username: %s, password: %s, %s", *username, *password, err.Error()) + } + + welcome(interactive) + + defer bye(*username, interactive) + defer client.Disconnect() + + // Loop the request + var exit error = nil + if interactive { + exit = loop(client, NewiCli(historyHome, *username)) + } else if *script != "" { + exit = loop(client, NewnCli(strings.NewReader(*script))) + } else if *file != "" { + fd, err := os.Open(*file) + if err != nil { + log.Fatalf("Open file %s failed, %s", *file, err.Error()) + } + exit = loop(client, NewnCli(fd)) + fd.Close() + } + + if exit != nil { + os.Exit(1) + } +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..21768a2 --- /dev/null +++ b/table.go @@ -0,0 +1,229 @@ +/* Copyright (c) 2020 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License, + * attached with Common Clause Condition 1.0, found in the LICENSES directory. + */ + +package main + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + common "github.com/vesoft-inc/nebula-go/v2/nebula" +) + +func val2String(value *common.Value, depth uint) string { + // TODO(shylock) get golang runtime limit + if depth == 0 { // Avoid too deep recursive + return "..." + } + + if value.IsSetNVal() { // null + switch value.GetNVal() { + case common.NullType___NULL__: + return "NULL" + case common.NullType_NaN: + return "NaN" + case common.NullType_BAD_DATA: + return "BAD_DATA" + case common.NullType_BAD_TYPE: + return "BAD_TYPE" + } + return "NULL" + } else if value.IsSetBVal() { // bool + return strconv.FormatBool(value.GetBVal()) + } else if value.IsSetIVal() { // int64 + return strconv.FormatInt(value.GetIVal(), 10) + } else if value.IsSetFVal() { // float64 + return strconv.FormatFloat(value.GetFVal(), 'g', -1, 64) + } else if value.IsSetSVal() { // string + return "\"" + string(value.GetSVal()) + "\"" + } else if value.IsSetDVal() { // yyyy-mm-dd + date := value.GetDVal() + str := fmt.Sprintf("%d-%d-%d", date.GetYear(), date.GetMonth(), date.GetDay()) + return str + } else if value.IsSetTVal() { // yyyy-mm-dd HH:MM:SS:MS TZ + datetime := value.GetTVal() + str := fmt.Sprintf("%d-%d-%d %d:%d:%d:%d UTC%d", + datetime.GetYear(), datetime.GetMonth(), datetime.GetDay(), + datetime.GetHour(), datetime.GetMinute(), datetime.GetSec(), datetime.GetMicrosec(), + datetime.GetTimezone()) + return str + } else if value.IsSetVVal() { // Vertex + var buffer bytes.Buffer + vertex := value.GetVVal() + buffer.WriteString("(") + buffer.WriteString(string(vertex.GetVid())) + buffer.WriteString(")") + buffer.WriteString(" ") + filled := false + for _, tag := range vertex.GetTags() { + tagName := string(tag.GetName()) + for k, v := range tag.GetProps() { + filled = true + buffer.WriteString(tagName) + buffer.WriteString(".") + buffer.WriteString(k) + buffer.WriteString(":") + buffer.WriteString(val2String(v, depth-1)) + buffer.WriteString(",") + } + } + if filled { + // remove last , + buffer.Truncate(buffer.Len() - 1) + } + return buffer.String() + } else if value.IsSetEVal() { // Edge + // src-[TypeName]->dst@ranking + edge := value.GetEVal() + var buffer bytes.Buffer + filled := false + buffer.WriteString(fmt.Sprintf("%s-[%s]->%s@%d", string(edge.GetSrc()), edge.GetName(), string(edge.GetDst()), + edge.GetRanking())) + buffer.WriteString(" ") + for k, v := range edge.GetProps() { + filled = true + buffer.WriteString(k) + buffer.WriteString(":") + buffer.WriteString(val2String(v, depth-1)) + buffer.WriteString(",") + } + if filled { + buffer.Truncate(buffer.Len() - 1) + } + return buffer.String() + } else if value.IsSetPVal() { // Path + // src-[TypeName]->dst@ranking-[TypeName]->dst@ranking ... + p := value.GetPVal() + str := string(p.GetSrc().GetVid()) + for _, step := range p.GetSteps() { + pStr := fmt.Sprintf("-[%s]->%s@%d", step.GetName(), string(step.GetDst().GetVid()), step.GetRanking()) + str += pStr + } + return str + } else if value.IsSetLVal() { // List + // TODO(shylock) optimize the recursive + l := value.GetLVal() + var buffer bytes.Buffer + buffer.WriteString("[") + for _, v := range l.GetValues() { + buffer.WriteString(val2String(v, depth-1)) + buffer.WriteString(",") + } + if buffer.Len() > 1 { + buffer.Truncate(buffer.Len() - 1) + } + buffer.WriteString("]") + return buffer.String() + } else if value.IsSetMVal() { // Map + // TODO(shylock) optimize the recursive + m := value.GetMVal() + var buffer bytes.Buffer + buffer.WriteString("{") + for k, v := range m.GetKvs() { + buffer.WriteString("\"" + k + "\"") + buffer.WriteString(":") + buffer.WriteString(val2String(v, depth-1)) + buffer.WriteString(",") + } + if buffer.Len() > 1 { + buffer.Truncate(buffer.Len() - 1) + } + buffer.WriteString("}") + return buffer.String() + } else if value.IsSetUVal() { // Set + // TODO(shylock) optimize the recursive + s := value.GetUVal() + var buffer bytes.Buffer + buffer.WriteString("{") + for _, v := range s.GetValues() { + buffer.WriteString(val2String(v, depth-1)) + buffer.WriteString(",") + } + if buffer.Len() > 1 { + buffer.Truncate(buffer.Len() - 1) + } + buffer.WriteString("}") + return buffer.String() + } + return "" +} + +func max(v1 uint, v2 uint) uint { + if v1 > v2 { + return v1 + } + return v2 +} + +func sum(a []uint) uint { + s := uint(0) + for _, v := range a { + s += v + } + return s +} + +type Table struct { + align uint // Each column align indent to boundary + headerChar string // Header line characters + rowChar string // Row line characters + colDelimiter string // Column delemiter +} + +func NewTable(align uint, header string, row string, delemiter string) Table { + return Table{align, header, row, delemiter} +} + +// Columns width +type TableSpec = []uint +type TableRows = [][]string + +func (t Table) printRow(row []string, colSpec TableSpec) { + for i, col := range row { + colString := t.colDelimiter + strings.Repeat(" ", int(t.align)) + col + length := uint(len(col)) + if length < colSpec[i]+t.align { + colString = colString + strings.Repeat(" ", int(colSpec[i]+t.align-length)) + } + fmt.Print(colString) + } + fmt.Println(t.colDelimiter) +} + +func (t Table) PrintTable(table *common.DataSet) { + columnSize := len(table.GetColumnNames()) + rowSize := len(table.GetRows()) + tableSpec := make(TableSpec, columnSize) + tableRows := make(TableRows, rowSize) + tableHeader := make([]string, columnSize) + for i, header := range table.GetColumnNames() { + tableSpec[i] = uint(len(header)) + tableHeader[i] = string(header) + } + for i, row := range table.GetRows() { + tableRows[i] = make([]string, columnSize) + for j, col := range row.GetValues() { + tableRows[i][j] = val2String(col, 256) + tableSpec[j] = max(uint(len(tableRows[i][j])), tableSpec[j]) + } + } + + // value limit + two indent + '|' itself + totalLineLength := int(sum(tableSpec)) + columnSize*int(t.align)*2 + columnSize + 1 + headerLine := strings.Repeat(t.headerChar, totalLineLength) + rowLine := strings.Repeat(t.rowChar, totalLineLength) + fmt.Println(headerLine) + t.printRow(tableHeader, tableSpec) + fmt.Println(headerLine) + for _, row := range tableRows { + t.printRow(row, tableSpec) + fmt.Println(rowLine) + } + fmt.Printf("Got %d rows, %d columns.", rowSize, columnSize) + fmt.Println() +}