Commit caa35353 authored by gwenn's avatar gwenn

First draft of a completion cache:

Each database (main, temp, ...) has its own cache versioned.
Tables and views are indexed by lowercase name.
parent 77b099d6
......@@ -2,29 +2,135 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build all
package sqlite
package shell
import (
"fmt"
"sort"
"strings"
"github.com/gwenn/gosqlite"
"github.com/sauerbraten/radix"
)
type completionCache struct {
dbNames []string // "main", "temp", ...
dbNames []string // "main", "temp", ...
dbCaches map[string]*databaseCache
}
type databaseCache struct {
schemaVersion int //
tableNames []string
viewNames []string
columnNames map[string][]string
schemaVersion int //
tableNames map[string]string // lowercase name => original name
viewNames map[string]string
columnNames map[string][]string // lowercase table name => column name
// idxNames []string // indexed by dbName (seems useful only in DROP INDEX statement)
// trigNames []string // trigger by dbName (seems useful only in DROP TRIGGER statement)
}
func CreateCache(db *sqlite.Conn) *completionCache {
return &completionCache{dbNames: make([]string, 0, 2), dbCaches: make(map[string]*databaseCache)}
}
func (cc *completionCache) Update(db *sqlite.Conn) error {
// update database list (TODO only on ATTACH ...)
cc.dbNames = cc.dbNames[:0]
dbNames, err := db.Databases()
if err != nil {
return err
}
// update databases cache
for dbName, _ := range dbNames {
cc.dbNames = append(cc.dbNames, dbName)
dbc := cc.dbCaches[dbName]
if dbc == nil {
dbc = &databaseCache{schemaVersion: -1, tableNames: make(map[string]string), viewNames: make(map[string]string), columnNames: make(map[string][]string)}
cc.dbCaches[dbName] = dbc
}
err = dbc.update(db, dbName)
if err != nil {
return err
}
}
return nil
}
func (dc *databaseCache) update(db *sqlite.Conn, dbName string) error {
var sv int
if sv, err := db.SchemaVersion(dbName); err != nil {
return err
} else if sv == dc.schemaVersion { // up to date
return nil
}
ts, err := db.Tables(dbName)
if err != nil {
return err
}
if dbName == "temp" {
ts = append(ts, "sqlite_temp_master")
} else {
ts = append(ts, "sqlite_master")
}
// clear
for table := range dc.tableNames {
delete(dc.tableNames, table)
}
for _, table := range ts {
dc.tableNames[strings.ToLower(table)] = table // TODO unicode
}
vs, err := db.Views(dbName)
if err != nil {
return err
}
// clear
for view := range dc.viewNames {
delete(dc.viewNames, view)
}
for _, view := range vs {
dc.viewNames[strings.ToLower(view)] = view // TODO unicode
}
// drop
for table := range dc.columnNames {
if _, ok := dc.tableNames[table]; ok {
continue
} else if _, ok := dc.viewNames[table]; ok {
continue
}
delete(dc.columnNames, table)
}
for table := range dc.tableNames {
cs, err := db.Columns(dbName, table)
if err != nil {
return err
}
columnNames := dc.columnNames[table]
columnNames = columnNames[:0]
for _, c := range cs {
columnNames = append(columnNames, c.Name)
}
dc.columnNames[table] = columnNames
}
for view := range dc.viewNames {
cs, err := db.Columns(dbName, view)
if err != nil {
return err
}
columnNames := dc.columnNames[view]
columnNames = columnNames[:0]
for _, c := range cs {
columnNames = append(columnNames, c.Name)
}
dc.columnNames[view] = columnNames
}
dc.schemaVersion = sv
fmt.Printf("%#v\n", dc)
return nil
}
var pragmaNames = radix.New()
// Only built-in functions are supported.
......@@ -35,6 +141,8 @@ var funcNames = radix.New()
// TODO make possible to register extended/user-defined modules
var moduleNames = radix.New()
var cmdNames = radix.New()
func init() {
radixSet(pragmaNames, "application_id", "integer")
radixSet(pragmaNames, "auto_vacuum", "0 | NONE | 1 | FULL | 2 | INCREMENTAL")
......@@ -153,6 +261,42 @@ func init() {
radixSet(moduleNames, "fts3(", "")
radixSet(moduleNames, "fts4(", "")
radixSet(moduleNames, "rtree(", "")
radixSet(cmdNames, ".backup", "?DB? FILE")
radixSet(cmdNames, ".bail", "ON|OFF")
radixSet(cmdNames, ".clone", "NEWDB")
radixSet(cmdNames, ".databases", "")
radixSet(cmdNames, ".dump", "?TABLE? ...")
radixSet(cmdNames, ".echo", "ON|OFF")
radixSet(cmdNames, ".exit", "")
radixSet(cmdNames, ".explain", "?ON|OFF?")
//radixSet(cmdNames, ".header", "ON|OFF")
radixSet(cmdNames, ".headers", "ON|OFF")
radixSet(cmdNames, ".help", "")
radixSet(cmdNames, ".import", "FILE TABLE")
radixSet(cmdNames, ".indices", "?TABLE?")
radixSet(cmdNames, ".load", "FILE ?ENTRY?")
radixSet(cmdNames, ".log", "FILE|off")
radixSet(cmdNames, ".mode", "MODE ?TABLE?")
radixSet(cmdNames, ".nullvalue", "STRING")
radixSet(cmdNames, ".open", "?FILENAME?")
radixSet(cmdNames, ".output", "stdout | FILENAME")
radixSet(cmdNames, ".print", "STRING...")
radixSet(cmdNames, ".prompt", "MAIN CONTINUE")
radixSet(cmdNames, ".quit", "")
radixSet(cmdNames, ".read", "FILENAME")
radixSet(cmdNames, ".restore", "?DB? FILE")
radixSet(cmdNames, ".save", "FILE")
radixSet(cmdNames, ".schema", "?TABLE?")
radixSet(cmdNames, ".separator", "STRING")
radixSet(cmdNames, ".show", "")
radixSet(cmdNames, ".stats", "ON|OFF")
radixSet(cmdNames, ".tables", "?TABLE?")
radixSet(cmdNames, ".timeout", "MS")
radixSet(cmdNames, ".trace", "FILE|off")
radixSet(cmdNames, ".vfsname", "?AUX?")
radixSet(cmdNames, ".width", "NUM1 NUM2 ...")
radixSet(cmdNames, ".timer", "ON|OFF")
}
type radixValue struct {
......@@ -170,6 +314,9 @@ func CompletePragma(prefix string) []string {
func CompleteFunc(prefix string) []string {
return complete(funcNames, prefix)
}
func CompleteCmd(prefix string) []string {
return complete(cmdNames, prefix)
}
func complete(root *radix.Radix, prefix string) []string {
r := root.SubTreeWithPrefix(prefix)
......
......@@ -2,15 +2,14 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build all
package sqlite_test
package shell_test
import (
"testing"
"github.com/bmizerany/assert"
. "github.com/gwenn/gosqlite"
"github.com/gwenn/gosqlite"
. "github.com/gwenn/gosqlite/shell"
)
func TestPragmaNames(t *testing.T) {
......@@ -23,3 +22,16 @@ func TestFuncNames(t *testing.T) {
assert.Equal(t, 2, len(funcs), "got %d functions; expected %d", len(funcs), 2)
assert.Equal(t, []string{"substr(", "sum("}, funcs, "unexpected functions")
}
func TestCmdNames(t *testing.T) {
cmds := CompleteCmd(".h")
assert.Equal(t, 2, len(cmds), "got %d commands; expected %d", len(cmds), 2)
assert.Equal(t, []string{".headers", ".help"}, cmds, "unexpected commands")
}
func TestCache(t *testing.T) {
db, err := sqlite.Open(":memory:")
assert.Tf(t, err == nil, "%v", err)
defer db.Close()
cc := CreateCache(db)
err = cc.Update(db)
assert.Tf(t, err == nil, "%v", err)
}
......@@ -15,11 +15,13 @@ import (
"os/signal"
"os/user"
"path"
"strings"
"syscall"
"text/tabwriter"
"unicode"
"github.com/gwenn/gosqlite"
"github.com/gwenn/gosqlite/shell"
"github.com/gwenn/liner"
)
......@@ -116,7 +118,32 @@ func catchInterrupt() {
signal.Notify(ch, syscall.SIGINT)
}
func completion(line string, pos int) (string, []string, string) {
if isBlank(line) {
return line[:pos], nil, line[pos:]
}
prefix := line[:pos]
var matches []string
if isCommand(line) {
i := strings.LastIndex(prefix, " ")
if i == -1 {
matches = shell.CompleteCmd(prefix)
if len(matches) > 0 {
prefix = ""
}
}
} else {
fields := strings.Fields(prefix)
if strings.EqualFold("PRAGMA", fields[0]) { // TODO check pos
matches = shell.CompletePragma(fields[1])
}
}
return prefix, matches, line[pos:]
}
func main() {
var err error
check(err)
if !liner.IsTerminal() {
return // TODO non-interactive mode
}
......@@ -129,7 +156,7 @@ func main() {
}
state.Close()
}()
// TODO state.SetCompleter(completion)
state.SetWordCompleter(completion)
err = loadHistory(state, historyFileName)
check(err)
......@@ -219,3 +246,41 @@ func main() {
b.Reset()
}
}
/*
.backup ?DB? FILE Backup DB (default "main") to FILE => NewBackup(dst, "main", db, ?DB?)
.bail ON|OFF Stop after hitting an error. Default OFF
.clone NEWDB Clone data into NEWDB from the existing database => ???
.databases List names and files of attached databases => db.Databases
.dump ?TABLE? ... Dump the database in an SQL text format => ???
.echo ON|OFF Turn command echo on or off
.exit Exit this program => *
.explain ?ON|OFF? Turn output mode suitable for EXPLAIN on or off.
.header(s) ON|OFF Turn display of headers on or off => *
.help Show this message => *
.import FILE TABLE Import data from FILE into TABLE => ImportCSV(FILE, ImportConfig, ???, TABLE) (TABLE may be qualified)
.indices ?TABLE? Show names of all indices => db.Indexes("main", both) + filter
.load FILE ?ENTRY? Load an extension library => db.LoadExtension(FILE, ?ENTRY?)
.log FILE|off Turn logging on or off. FILE can be stderr/stdout
.mode MODE ?TABLE? Set output mode
.nullvalue STRING Use STRING in place of NULL values
.open ?FILENAME? Close existing database and reopen FILENAME
.output FILENAME Send output to FILENAME => *
.output stdout Send output to the screen => *
.print STRING... Print literal STRING
.prompt MAIN CONTINUE Replace the standard prompts
.quit Exit this program => *
.read FILENAME Execute SQL in FILENAME => *
.restore ?DB? FILE Restore content of DB (default "main") from FILE => NewBackup(db, ?DB?, src, "main")
.save FILE Write in-memory database into FILE => NewBackup(dst, "main", db, "main")
.schema ?TABLE? Show the CREATE statements => *
.separator STRING Change separator used by output mode and .import => *
.show Show the current values for various settings
.stats ON|OFF Turn stats on or off
.tables ?TABLE? List names of tables => *
.timeout MS Try opening locked tables for MS milliseconds
.trace FILE|off Output each SQL statement as it is run => db.Trace(???, nil)
.vfsname ?AUX? Print the name of the VFS stack
.width NUM1 NUM2 ... Set column widths for "column" mode
.timer ON|OFF Turn the CPU timer measurement on or off
*/
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment