Skip to content

Commit

Permalink
sql: add SHOW COMPLETIONS AT offset FOR syntax
Browse files Browse the repository at this point in the history
Release note (sql change): Support
SHOW COMPLETIONS AT OFFSET <offset> FOR <stmt> syntax that
returns a set of SQL keywords that can complete the keyword at
<offset> in the given <stmt>.

If the offset is in the middle of a word, then it returns the
full word.
For example SHOW COMPLETIONS AT OFFSET 1 FOR "SELECT" returns select.
  • Loading branch information
RichardJCai committed Jan 12, 2022
1 parent 2de17e7 commit 2a70cdc
Show file tree
Hide file tree
Showing 10 changed files with 336 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/generated/sql/bnf/stmt_block.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,7 @@ unreserved_keyword ::=
| 'COMMITTED'
| 'COMPACT'
| 'COMPLETE'
| 'COMPLETIONS'
| 'CONFLICT'
| 'CONFIGURATION'
| 'CONFIGURATIONS'
Expand Down
3 changes: 3 additions & 0 deletions pkg/sql/delegate/delegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ func TryDelegate(
case *tree.ShowSchedules:
return d.delegateShowSchedules(t)

case *tree.ShowCompletions:
return d.delegateShowCompletions(t)

case *tree.ControlJobsForSchedules:
return d.delegateJobControl(ControlJobsDelegate{
Schedules: t.Schedules,
Expand Down
122 changes: 122 additions & 0 deletions pkg/sql/delegate/show_completions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package delegate

import (
"bytes"
"fmt"
"sort"
"strconv"
"strings"
"unicode"

"github.com/cockroachdb/cockroach/pkg/sql/lexbase"
"github.com/cockroachdb/cockroach/pkg/sql/parser"
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
"github.com/cockroachdb/errors"
)

func (d *delegator) delegateShowCompletions(n *tree.ShowCompletions) (tree.Statement, error) {
offsetVal, ok := n.Offset.AsConstantInt()
if !ok {
return nil, errors.Newf("invalid offset %v", n.Offset)
}
offset, err := strconv.Atoi(offsetVal.String())
if err != nil {
return nil, err
}

completions, err := runShowCompletions(n.Statement, offset)
if err != nil {
return nil, err
}

if len(completions) == 0 {
return parse(`SELECT '' as completions`)
}

var query bytes.Buffer
fmt.Fprint(&query, "SELECT @1 AS completions FROM (VALUES ")

comma := ""
for _, completion := range completions {
fmt.Fprintf(&query, "%s(", comma)
lexbase.EncodeSQLString(&query, completion)
query.WriteByte(')')
comma = ", "
}

fmt.Fprintf(&query, ")")

return parse(query.String())
}

func runShowCompletions(stmt string, offset int) ([]string, error) {
if offset <= 0 || offset > len(stmt) {
return nil, nil
}

// For simplicity, if we're on a whitespace, return no completions.
// Currently, parser.
// parser.TokensIgnoreErrors does not consider whitespaces
// after the last token.
// Ie "SELECT ", will only return one token being "SELECT".
// If we're at the whitespace, we do not want to return completion
// recommendations for "SELECT".
if unicode.IsSpace([]rune(stmt)[offset-1]) {
return nil, nil
}

sqlTokens := parser.TokensIgnoreErrors(string([]rune(stmt)[:offset]))
if len(sqlTokens) == 0 {
return nil, nil
}

sqlTokenStrings := make([]string, len(sqlTokens))
for i, sqlToken := range sqlTokens {
sqlTokenStrings[i] = sqlToken.Str
}

lastWordTruncated := sqlTokenStrings[len(sqlTokenStrings)-1]

// If the offset is in the middle of a word, we return the full word.
// For example if the stmt is SELECT with offset 2, even though SEARCH would
// come first for "SE", we want to return "SELECT".
// Similarly, if we have SEL with offset 2, we want to return "SEL".
allSqlTokens := parser.TokensIgnoreErrors(stmt)
lastWordFull := allSqlTokens[len(sqlTokenStrings)-1]
if lastWordFull.Str != lastWordTruncated {
return []string{strings.ToUpper(lastWordFull.Str)}, nil
}

return getCompletionsForWord(lastWordTruncated, lexbase.KeywordNames), nil
}

// Binary search for range with matching prefixes
// Return the range of matching prefixes for w.
func binarySearch(w string, words []string) (int, int) {
// First binary search for the first string in the sorted lexbase.KeywordNames list
// that matches the prefix w.
left := sort.Search(len(words), func(i int) bool { return words[i] >= w })

// Binary search for the last string in the sorted lexbase.KeywordNames list that matches
// the prefix w.
right := sort.Search(len(words), func(i int) bool { return words[i][:min(len(words[i]), len(w))] > w })

return left, right
}

func min(a int, b int) int {
if a < b {
return a
}
return b
}

func getCompletionsForWord(w string, words []string) []string {
left, right := binarySearch(strings.ToLower(w), words)
completions := make([]string, right-left)

for i, word := range words[left:right] {
completions[i] = strings.ToUpper(word)
}
return completions
}
95 changes: 95 additions & 0 deletions pkg/sql/delegate/show_completions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package delegate

import (
"reflect"
"testing"
)

func TestCompletions(t *testing.T) {
tests := []struct {
stmt string
offset int
expectedCompletions []string
}{
{
stmt: "creat",
expectedCompletions: []string{"CREATE", "CREATEDB", "CREATELOGIN", "CREATEROLE"},
},
{
stmt: "CREAT",
expectedCompletions: []string{"CREATE", "CREATEDB", "CREATELOGIN", "CREATEROLE"},
},
{
stmt: "creat ",
expectedCompletions: []string{},
},
{
stmt: "SHOW CREAT",
expectedCompletions: []string{"CREATE", "CREATEDB", "CREATELOGIN", "CREATEROLE"},
},
{
stmt: "show creat",
expectedCompletions: []string{"CREATE", "CREATEDB", "CREATELOGIN", "CREATEROLE"},
},
{
stmt: "se",
expectedCompletions: []string{
"SEARCH", "SECOND", "SELECT", "SEQUENCE", "SEQUENCES",
"SERIALIZABLE", "SERVER", "SESSION", "SESSIONS", "SESSION_USER",
"SET", "SETS", "SETTING", "SETTINGS",
},
},
{
stmt: "sel",
expectedCompletions: []string{"SELECT"},
},
{
stmt: "create ta",
expectedCompletions: []string{"TABLE", "TABLES", "TABLESPACE"},
},
{
stmt: "create ta",
expectedCompletions: []string{"CREATE"},
offset: 3,
},
{
stmt: "select",
expectedCompletions: []string{"SELECT"},
offset: 2,
},
{
stmt: "select ",
expectedCompletions: []string{},
offset: 7,
},
{
stmt: "你好,我的名字是鲍勃 SELECT",
expectedCompletions: []string{"你好,我的名字是鲍勃"},
offset: 2,
},
{
stmt: "你好,我的名字是鲍勃 SELECT",
expectedCompletions: []string{},
offset: 11,
},
{
stmt: "你好,我的名字是鲍勃 SELECT",
expectedCompletions: []string{"SELECT"},
offset: 12,
},
}
for _, tc := range tests {
offset := tc.offset
if tc.offset == 0 {
offset = len(tc.stmt)
}
completions, err := runShowCompletions(tc.stmt, offset)
if err != nil {
t.Error(err)
}
if !(len(completions) == 0 && len(tc.expectedCompletions) == 0) &&
!reflect.DeepEqual(completions, tc.expectedCompletions) {
t.Errorf("expected %v, got %v", tc.expectedCompletions, completions)
}
}
}
1 change: 1 addition & 0 deletions pkg/sql/delegate/show_syntax.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ func (d *delegator) delegateShowSyntax(n *tree.ShowSyntax) (tree.Statement, erro
nil, /* reportErr */
)
query.WriteByte(')')

return parse(query.String())
}
61 changes: 61 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/show_completions
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
query T
show completions at offset 1 for 'select 1'
----
SELECT

query T
show completions at offset 7 for 'select 1'
----
·

query T
show completions at offset 7 for 'select 2'
----
·

query T
show completions at offset 10 for 'select * fro'
----
FRO

query T
show completions at offset 11 for 'select * fro'
----
FRO

query T
show completions at offset 12 for 'select * fro'
----
FROM

query T
show completions at offset 10 for 'select * from'
----
FROM

query T
show completions at offset 11 for 'select * from'
----
FROM

# This case doesn't really make sense - completing this as SELECT doesn't
# really make sense but we'll need to add more complex logic to determine
# whether our SQL token is a string const.
# However we do want to test this so we can ensure we handle escaped strings.
query T
show completions at offset 4 for e'\'se\'';
----
SEARCH
SECOND
SELECT
SEQUENCE
SEQUENCES
SERIALIZABLE
SERVER
SESSION
SESSIONS
SESSION_USER
SET
SETS
SETTING
SETTINGS
15 changes: 15 additions & 0 deletions pkg/sql/parser/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ func Tokens(sql string) (tokens []TokenString, ok bool) {
return tokens, true
}

// TokensIgnoreErrors decomposes the input into lexical tokens and
// ignores errors.
func TokensIgnoreErrors(sql string) (tokens []TokenString) {
s := makeScanner(sql)
for {
var lval = &sqlSymType{}
s.Scan(lval)
if lval.ID() == 0 {
break
}
tokens = append(tokens, TokenString{TokenID: lval.ID(), Str: lval.Str()})
}
return tokens
}

// TokenString is the unit value returned by Tokens.
type TokenString struct {
TokenID int32
Expand Down
15 changes: 14 additions & 1 deletion pkg/sql/parser/sql.y
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ func (u *sqlSymUnion) setVar() *tree.SetVar {
%token <str> CACHE CANCEL CANCELQUERY CASCADE CASE CAST CBRT CHANGEFEED CHAR
%token <str> CHARACTER CHARACTERISTICS CHECK CLOSE
%token <str> CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
%token <str> COMMITTED COMPACT COMPLETE CONCAT CONCURRENTLY CONFIGURATION CONFIGURATIONS CONFIGURE
%token <str> COMMITTED COMPACT COMPLETE COMPLETIONS CONCAT CONCURRENTLY CONFIGURATION CONFIGURATIONS CONFIGURE
%token <str> CONFLICT CONNECTION CONSTRAINT CONSTRAINTS CONTAINS CONTROLCHANGEFEED CONTROLJOB
%token <str> CONVERSION CONVERT COPY COVERING CREATE CREATEDB CREATELOGIN CREATEROLE
%token <str> CROSS CSV CUBE CURRENT CURRENT_CATALOG CURRENT_DATE CURRENT_SCHEMA
Expand Down Expand Up @@ -1091,6 +1091,7 @@ func (u *sqlSymUnion) setVar() *tree.SetVar {
%type <tree.Statement> show_zone_stmt
%type <tree.Statement> show_schedules_stmt
%type <tree.Statement> show_full_scans_stmt
%type <tree.Statement> show_completions_stmt

%type <str> statements_or_queries

Expand Down Expand Up @@ -4911,6 +4912,7 @@ show_stmt:
| show_last_query_stats_stmt
| show_full_scans_stmt
| show_default_privileges_stmt // EXTEND WITH HELP: SHOW DEFAULT PRIVILEGES
| show_completions_stmt

// Cursors are not yet supported by CockroachDB. CLOSE ALL is safe to no-op
// since there will be no open cursors.
Expand Down Expand Up @@ -5575,6 +5577,16 @@ show_syntax_stmt:
}
| SHOW SYNTAX error // SHOW HELP: SHOW SYNTAX

show_completions_stmt:
SHOW COMPLETIONS AT OFFSET ICONST FOR SCONST
{
/* SKIP DOC */
$$.val = &tree.ShowCompletions{
Statement: $7,
Offset: $5.numVal(),
}
}

show_last_query_stats_stmt:
SHOW LAST QUERY STATISTICS query_stats_cols
{
Expand Down Expand Up @@ -13112,6 +13124,7 @@ unreserved_keyword:
| COMMITTED
| COMPACT
| COMPLETE
| COMPLETIONS
| CONFLICT
| CONFIGURATION
| CONFIGURATIONS
Expand Down
14 changes: 14 additions & 0 deletions pkg/sql/sem/tree/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -885,3 +885,17 @@ func (n *ShowDefaultPrivileges) Format(ctx *FmtCtx) {
ctx.WriteString("FOR ALL ROLES ")
}
}

type ShowCompletions struct {
Statement string
Offset *NumVal
}

func (s ShowCompletions) Format(ctx *FmtCtx) {
ctx.WriteString("SHOW COMPLETIONS AT OFFSET ")
s.Offset.Format(ctx)
ctx.WriteString(" FOR ")
ctx.WriteString(lexbase.EscapeSQLString(s.Statement))
}

var _ Statement = &ShowCompletions{}
Loading

0 comments on commit 2a70cdc

Please sign in to comment.