syncbase/sb51: Make query demo tool a proper syncbase client.

Replaced query/demo with 'sb51 sh', the first version of a
Syncbase generic client and admin tool.
'sb51 sh' connects to a database on a Syncbase instance,
optionally creating and populating it with demo tables, and
opens a SyncQL shell.
Added abort on error when the shell is not connected to a tty.

Change-Id: I8290df33abdceb49716cb99acd2cb4f25622d84c
diff --git a/v23/syncbase/nosql/internal/query/demo/db/db.go b/v23/syncbase/nosql/internal/query/demo/db/db.go
deleted file mode 100644
index 9ec201a..0000000
--- a/v23/syncbase/nosql/internal/query/demo/db/db.go
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package db
-
-import (
-	"errors"
-	"fmt"
-	"time"
-
-	"v.io/syncbase/v23/syncbase/nosql/query_db"
-	"v.io/v23"
-	"v.io/v23/context"
-	"v.io/v23/vdl"
-	_ "v.io/x/ref/runtime/factories/generic"
-	"v.io/x/ref/test"
-)
-
-var d *demoDB
-
-func InitDB() v23.Shutdown {
-	d = createDB()
-	var shutdown v23.Shutdown
-	d.ctx, shutdown = test.V23Init()
-	return shutdown
-}
-
-type demoDB struct {
-	ctx    *context.T
-	tables []table
-}
-
-type kv struct {
-	key   string
-	value *vdl.Value
-}
-
-type table struct {
-	name string
-	rows []kv
-}
-
-type keyValueStreamImpl struct {
-	table       table
-	cursor      int
-	keyRanges   query_db.KeyRanges
-	rangeCursor int
-}
-
-func compareKeyToLimit(key, limit string) int {
-	if limit == "" || key < limit {
-		return -1
-	} else if key == limit {
-		return 0
-	} else {
-		return 1
-	}
-}
-
-func (kvs *keyValueStreamImpl) Advance() bool {
-	for true {
-		kvs.cursor++ // initialized to -1
-		if kvs.cursor >= len(kvs.table.rows) {
-			return false
-		}
-		// does it match any keyRange
-		for kvs.rangeCursor < len(kvs.keyRanges) {
-			if kvs.table.rows[kvs.cursor].key >= kvs.keyRanges[kvs.rangeCursor].Start && compareKeyToLimit(kvs.table.rows[kvs.cursor].key, kvs.keyRanges[kvs.rangeCursor].Limit) < 0 {
-				return true
-			}
-			// Keys and keyRanges are both sorted low to high, so we can increment
-			// rangeCursor if the keyRange.Limit is < the key.
-			if kvs.keyRanges[kvs.rangeCursor].Limit < kvs.table.rows[kvs.cursor].key {
-				kvs.rangeCursor++
-				if kvs.rangeCursor >= len(kvs.keyRanges) {
-					return false
-				}
-			} else {
-				break
-			}
-		}
-	}
-	return false
-}
-
-func (kvs *keyValueStreamImpl) KeyValue() (string, *vdl.Value) {
-	return kvs.table.rows[kvs.cursor].key, kvs.table.rows[kvs.cursor].value
-}
-
-func (kvs *keyValueStreamImpl) Err() error {
-	return nil
-}
-
-func (kvs *keyValueStreamImpl) Cancel() {
-}
-
-func (t table) Scan(keyRanges query_db.KeyRanges) (query_db.KeyValueStream, error) {
-	var keyValueStreamImpl keyValueStreamImpl
-	keyValueStreamImpl.table = t
-	keyValueStreamImpl.cursor = -1
-	keyValueStreamImpl.keyRanges = keyRanges
-	return &keyValueStreamImpl, nil
-}
-
-// GetTableNames is not part of the query_db.Database interface.
-// Tables are discovered outside the query package.
-// TODO(jkline): Consider system tables to discover info such as this.
-func GetTableNames() []string {
-	tables := []string{}
-	for _, table := range d.tables {
-		tables = append(tables, table.name)
-	}
-	return tables
-}
-
-// GetDatabase in not part of the query_db.Database interface.
-// Database instances are obtained outside the query package.
-func GetDatabase() query_db.Database {
-	return d
-}
-
-func (db demoDB) GetContext() *context.T {
-	return db.ctx
-}
-
-func (d demoDB) GetTable(table string) (query_db.Table, error) {
-	for _, t := range d.tables {
-		if t.name == table {
-			return t, nil
-		}
-	}
-	return nil, errors.New(fmt.Sprintf("No such table: %s.", table))
-}
-
-func createDB() *demoDB {
-	d = &demoDB{}
-	var custTable table
-	custTable.name = "Customer"
-	custTable.rows = []kv{
-		kv{
-			"001",
-			vdl.ValueOf(Customer{"John Smith", 1, true, AddressInfo{"1 Main St.", "Palo Alto", "CA", "94303"}, CreditReport{Agency: CreditAgencyEquifax, Report: AgencyReportEquifaxReport{EquifaxCreditReport{'A'}}}}),
-		},
-		kv{
-			"001001",
-			vdl.ValueOf(Invoice{1, 1000, 42, AddressInfo{"1 Main St.", "Palo Alto", "CA", "94303"}}),
-		},
-		kv{
-			"001002",
-			vdl.ValueOf(Invoice{1, 1003, 7, AddressInfo{"2 Main St.", "Palo Alto", "CA", "94303"}}),
-		},
-		kv{
-			"001003",
-			vdl.ValueOf(Invoice{1, 1005, 88, AddressInfo{"3 Main St.", "Palo Alto", "CA", "94303"}}),
-		},
-		kv{
-			"002",
-			vdl.ValueOf(Customer{"Bat Masterson", 2, true, AddressInfo{"777 Any St.", "Collins", "IA", "50055"}, CreditReport{Agency: CreditAgencyTransUnion, Report: AgencyReportTransUnionReport{TransUnionCreditReport{80}}}}),
-		},
-		kv{
-			"002001",
-			vdl.ValueOf(Invoice{2, 1001, 166, AddressInfo{"777 Any St.", "Collins", "IA", "50055"}}),
-		},
-		kv{
-			"002002",
-			vdl.ValueOf(Invoice{2, 1002, 243, AddressInfo{"888 Any St.", "Collins", "IA", "50055"}}),
-		},
-		kv{
-			"002003",
-			vdl.ValueOf(Invoice{2, 1004, 787, AddressInfo{"999 Any St.", "Collins", "IA", "50055"}}),
-		},
-		kv{
-			"002004",
-			vdl.ValueOf(Invoice{2, 1006, 88, AddressInfo{"101010 Any St.", "Collins", "IA", "50055"}}),
-		},
-	}
-	d.tables = append(d.tables, custTable)
-
-	var numTable table
-	numTable.name = "Numbers"
-	numTable.rows = []kv{
-		kv{
-			"001",
-			vdl.ValueOf(Numbers{byte(12), uint16(1234), uint32(5678), uint64(999888777666), int16(9876), int32(876543), int64(128), float32(3.14159), float64(2.71828182846), complex64(123.0 + 7.0i), complex128(456.789 + 10.1112i)}),
-		},
-		kv{
-			"002",
-			vdl.ValueOf(Numbers{byte(9), uint16(99), uint32(999), uint64(9999999), int16(9), int32(99), int64(88), float32(1.41421356237), float64(1.73205080757), complex64(9.87 + 7.65i), complex128(4.32 + 1.0i)}),
-		},
-		kv{
-			"003",
-			vdl.ValueOf(Numbers{byte(210), uint16(210), uint32(210), uint64(210), int16(210), int32(210), int64(210), float32(210.0), float64(210.0), complex64(210.0 + 0.0i), complex128(210.0 + 0.0i)}),
-		},
-	}
-	d.tables = append(d.tables, numTable)
-
-	var compositeTable table
-	compositeTable.name = "Composites"
-	compositeTable.rows = []kv{
-		kv{
-			"uno",
-			vdl.ValueOf(Composite{Array2String{"foo", "bar"}, []int32{1, 2}, map[int32]struct{}{1: struct{}{}, 2: struct{}{}}, map[string]int32{"foo": 1, "bar": 2}}),
-		},
-	}
-	d.tables = append(d.tables, compositeTable)
-
-	var recursiveTable table
-	recursiveTable.name = "Recursives"
-	recursiveTable.rows = []kv{
-		kv{
-			"alpha",
-			vdl.ValueOf(Recursive{nil, &Times{time.Unix(123456789, 42244224), time.Duration(1337)}, map[Array2String]Recursive{
-				Array2String{"a", "b"}: Recursive{},
-				Array2String{"x", "y"}: Recursive{vdl.ValueOf(CreditReport{Agency: CreditAgencyExperian, Report: AgencyReportExperianReport{ExperianCreditReport{ExperianRatingGood}}}), nil, map[Array2String]Recursive{
-					Array2String{"alpha", "beta"}: Recursive{vdl.ValueOf(FooType{Bar: BarType{Baz: BazType{Name: "hello", TitleOrValue: TitleOrValueTypeValue{Value: 42}}}}), nil, nil},
-				}},
-				Array2String{"u", "v"}: Recursive{vdl.ValueOf(vdl.TypeOf(Recursive{})), nil, nil},
-			}}),
-		},
-	}
-	d.tables = append(d.tables, recursiveTable)
-
-	return d
-}
diff --git a/v23/syncbase/nosql/internal/query/demo/db/doc.go b/v23/syncbase/nosql/internal/query/demo/db/doc.go
deleted file mode 100644
index e6a1508..0000000
--- a/v23/syncbase/nosql/internal/query/demo/db/doc.go
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// An in-memory implementation of query_db.
-package db
diff --git a/v23/syncbase/nosql/internal/query/demo/demo.go b/v23/syncbase/nosql/internal/query/demo/demo.go
deleted file mode 100644
index da46e61..0000000
--- a/v23/syncbase/nosql/internal/query/demo/demo.go
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"io"
-	"os"
-	"strconv"
-	"strings"
-
-	isatty "github.com/mattn/go-isatty"
-
-	"v.io/syncbase/v23/syncbase/nosql/internal/query"
-	"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db"
-	"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/reader"
-	"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/writer"
-	"v.io/syncbase/v23/syncbase/nosql/query_db"
-	_ "v.io/x/ref/runtime/factories/generic"
-)
-
-var (
-	format = flag.String("format", "table",
-		"Output format.  'table': human-readable table; 'csv': comma-separated values, use -csv-delimiter to control the delimiter; 'json': JSON objects.")
-	csvDelimiter = flag.String("csv-delimiter", ",", "Delimiter to use when printing data as CSV (e.g. \"\t\", \",\")")
-)
-
-func dumpDB(d query_db.Database) {
-	for _, table := range db.GetTableNames() {
-		fmt.Printf("table: %s\n", table)
-		queryExec(d, fmt.Sprintf("select k, v from %s", table))
-	}
-}
-
-// Split an error message into an offset and the remaining (i.e., rhs of offset) message.
-// The convention for syncql is "<module><optional-rpc>[offset}<remaining-message>".
-func splitError(err error) (int64, string) {
-	errMsg := err.Error()
-	idx1 := strings.Index(errMsg, "[")
-	idx2 := strings.Index(errMsg, "]")
-	if idx1 == -1 || idx2 == -1 {
-		return 0, errMsg
-	}
-	offsetString := errMsg[idx1+1 : idx2]
-	offset, err := strconv.ParseInt(offsetString, 10, 64)
-	if err != nil {
-		return 0, errMsg
-	}
-	return offset, errMsg[idx2+1:]
-}
-
-func queryExec(d query_db.Database, q string) {
-	if columnNames, rs, err := query.Exec(d, q); err != nil {
-		off, msg := splitError(err)
-		fmt.Fprintf(os.Stderr, "%s\n", q)
-		fmt.Fprintf(os.Stderr, "%s^\n", strings.Repeat(" ", int(off)))
-		fmt.Fprintf(os.Stderr, "Error: %s\n", msg)
-	} else {
-		if *format == "table" {
-			if err := writer.WriteTable(os.Stdout, columnNames, rs); err != nil {
-				fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
-			}
-		} else if *format == "csv" {
-			if err := writer.WriteCSV(os.Stdout, columnNames, rs, *csvDelimiter); err != nil {
-				fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
-			}
-		} else if *format == "json" {
-			if err := writer.WriteJson(os.Stdout, columnNames, rs); err != nil {
-				fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
-			}
-		} else {
-			panic(fmt.Sprintf("invalid format flag value: %v", *format))
-		}
-	}
-}
-
-func main() {
-	shutdown := db.InitDB()
-	defer shutdown()
-
-	if *format != "table" && *format != "csv" && *format != "json" {
-		fmt.Fprintf(os.Stderr, "Unsupported -format %q.  Must be one of 'table', 'csv', or 'json'.\n", *format)
-		os.Exit(-1)
-	}
-
-	var input *reader.T
-	isTerminal := isatty.IsTerminal(os.Stdin.Fd())
-	if isTerminal {
-		input = reader.NewInteractive()
-	} else {
-		input = reader.NewNonInteractive()
-	}
-	defer input.Close()
-
-	d := db.GetDatabase()
-	for true {
-		if q, err := input.GetQuery(); err != nil {
-			if err == io.EOF {
-				if isTerminal {
-					// ctrl-d
-					fmt.Println()
-				}
-				break
-			} else {
-				// ctrl-c
-				break
-			}
-		} else {
-			tq := strings.ToLower(strings.TrimSpace(q))
-			if tq == "" || tq == "exit" || tq == "quit" {
-				break
-			}
-			if tq == "dump" {
-				dumpDB(d)
-			} else {
-				queryExec(d, q)
-			}
-		}
-	}
-}
diff --git a/v23/syncbase/nosql/internal/query/demo/doc.go b/v23/syncbase/nosql/internal/query/demo/doc.go
deleted file mode 100644
index 28c7fba..0000000
--- a/v23/syncbase/nosql/internal/query/demo/doc.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2015 The Vanadium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// This demo program exercises the syncbase query language.
-// The program's in-memory database implements the following
-// interfaces in query_db:
-//     Database
-//     Table
-//     KeyValueStream
-//
-// The user can enter the following at the command line:
-//     1. dump - to get a dump of the database
-//     2. a syncbase select statement - which is executed and results printed to stdout
-//     3. exit (or empty line) - to exit the program
-//
-// To build example:
-//     v23 go install v.io/syncbase/v23/syncbase/nosql/internal/query/demo
-//
-// To run example:
-//     $V23_ROOT/roadmap/go/bin/demo
-//
-// Sample run:
-//     > $V23_ROOT/roadmap/go/bin/demo
-//     Enter query (or 'dump' or 'exit')? select v.Name, v.Address.State from Customer where t = "Customer"
-//     John Smith,CA
-//     Bat Masterson,IA
-//     Enter query (or 'dump' or 'exit')? select v.CustID, v.InvoiceNum, v.ShipTo.Zip, v.Amount from Customer where t = "Invoice" and v.Amount > 100
-//     2,1001,50055,166
-//     2,1002,50055,243
-//     2,1004,50055,787
-//     Enter query (or 'dump' or 'exit')? select k, v fro Customer
-//     select k, v fro Customer
-//                 ^
-//     Error: [Off:12] Expected 'from', found 'fro'
-//     Enter query (or 'dump' or 'exit')? select k, v from Customer
-//     001,{John Smith 1 true 65 {1 Main St. Palo Alto CA 94303}
-//     001001,{1 1000 42 {1 Main St. Palo Alto CA 94303}}
-//     001002,{1 1003 7 {2 Main St. Palo Alto CA 94303}}
-//     001003,{1 1005 88 {3 Main St. Palo Alto CA 94303}}
-//     002,{Bat Masterson 2 true 66 {777 Any St. Collins IA 50055}
-//     002001,{2 1001 166 {777 Any St. collins IA 50055}}
-//     002002,{2 1002 243 {888 Any St. collins IA 50055}}
-//     002003,{2 1004 787 {999 Any St. collins IA 50055}}
-//     002004,{2 1006 88 {101010 Any St. collins IA 50055}}
-//     exit
-//     >
-package main
diff --git a/x/ref/syncbase/sb51/doc.go b/x/ref/syncbase/sb51/doc.go
new file mode 100644
index 0000000..73609c1
--- /dev/null
+++ b/x/ref/syncbase/sb51/doc.go
@@ -0,0 +1,55 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Antimony (sb51) is a Syncbase general-purpose client and management utility.
+// It currently supports experimenting with the Syncbase query language.
+//
+// The 'sh' command connects to a specified database on a Syncbase instance,
+// creating it if it does not exist if -create-missing is specified.
+// The user can then enter the following at the command line:
+//     1. dump - to get a dump of the database
+//     2. a syncbase select statement - which is executed and results printed to stdout
+//     3. make-demo - to create demo tables in the database to experiment with, equivalent to -make-demo flag
+//     4. exit (or quit) - to exit the program
+//
+// When the shell is running non-interactively (stdin not connected to a tty),
+// errors cause the shell to exit with a non-zero status.
+//
+// To build client:
+//     v23 go install v.io/syncbase/x/ref/syncbase/sb51
+//
+// To run client:
+//     $V23_ROOT/roadmap/go/bin/sb51 sh <appname> <dbname>
+//
+// Sample run (assuming a syncbase service is mounted at '/:8101/syncbase',
+// otherwise specify using -service flag):
+//     > $V23_ROOT/roadmap/go/bin/sb51 sh -create-missing -make-demo -format=csv demoapp demodb
+//     ? select v.Name, v.Address.State from DemoCustomers where t = "Customer";
+//     v.Name,v.Address.State
+//     John Smith,CA
+//     Bat Masterson,IA
+//     ? select v.CustId, v.InvoiceNum, v.ShipTo.Zip, v.Amount from DemoCustomers where t = "Invoice" and v.Amount > 100;
+//     v.CustId,v.InvoiceNum,v.ShipTo.Zip,v.Amount
+//     2,1001,50055,166
+//     2,1002,50055,243
+//     2,1004,50055,787
+//     ? select k, v fro DemoCustomers;
+//     Error:
+//     select k, v fro DemoCustomers
+//                 ^
+//     13: Expected 'from', found fro.
+//     ? select k, v from DemoCustomers;
+//     k,v
+//     001,"{Name: ""John Smith"", Id: 1, Active: true, Address: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}, Credit: {Agency: Equifax, Report: EquifaxReport: {Rating: 65}}}"
+//     001001,"{CustId: 1, InvoiceNum: 1000, Amount: 42, ShipTo: {Street: ""1 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
+//     001002,"{CustId: 1, InvoiceNum: 1003, Amount: 7, ShipTo: {Street: ""2 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
+//     001003,"{CustId: 1, InvoiceNum: 1005, Amount: 88, ShipTo: {Street: ""3 Main St."", City: ""Palo Alto"", State: ""CA"", Zip: ""94303""}}"
+//     002,"{Name: ""Bat Masterson"", Id: 2, Active: true, Address: {Street: ""777 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}, Credit: {Agency: TransUnion, Report: TransUnionReport: {Rating: 80}}}"
+//     002001,"{CustId: 2, InvoiceNum: 1001, Amount: 166, ShipTo: {Street: ""777 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     002002,"{CustId: 2, InvoiceNum: 1002, Amount: 243, ShipTo: {Street: ""888 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     002003,"{CustId: 2, InvoiceNum: 1004, Amount: 787, ShipTo: {Street: ""999 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     002004,"{CustId: 2, InvoiceNum: 1006, Amount: 88, ShipTo: {Street: ""101010 Any St."", City: ""Collins"", State: ""IA"", Zip: ""50055""}}"
+//     ? exit;
+//     >
+package main
diff --git a/x/ref/syncbase/sb51/internal/demodb/db.go b/x/ref/syncbase/sb51/internal/demodb/db.go
new file mode 100644
index 0000000..2674361
--- /dev/null
+++ b/x/ref/syncbase/sb51/internal/demodb/db.go
@@ -0,0 +1,138 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package demodb
+
+import (
+	"fmt"
+	"time"
+
+	wire "v.io/syncbase/v23/services/syncbase/nosql"
+	"v.io/syncbase/v23/syncbase/nosql"
+	"v.io/v23/context"
+	"v.io/v23/vdl"
+)
+
+type kv struct {
+	key   string
+	value *vdl.Value
+}
+
+type table struct {
+	name string
+	rows []kv
+}
+
+const demoPrefix = "Demo"
+
+var demoTables = []table{
+	table{
+		name: "Customers",
+		rows: []kv{
+			kv{
+				"001",
+				vdl.ValueOf(Customer{"John Smith", 1, true, AddressInfo{"1 Main St.", "Palo Alto", "CA", "94303"}, CreditReport{Agency: CreditAgencyEquifax, Report: AgencyReportEquifaxReport{EquifaxCreditReport{'A'}}}}),
+			},
+			kv{
+				"001001",
+				vdl.ValueOf(Invoice{1, 1000, 42, AddressInfo{"1 Main St.", "Palo Alto", "CA", "94303"}}),
+			},
+			kv{
+				"001002",
+				vdl.ValueOf(Invoice{1, 1003, 7, AddressInfo{"2 Main St.", "Palo Alto", "CA", "94303"}}),
+			},
+			kv{
+				"001003",
+				vdl.ValueOf(Invoice{1, 1005, 88, AddressInfo{"3 Main St.", "Palo Alto", "CA", "94303"}}),
+			},
+			kv{
+				"002",
+				vdl.ValueOf(Customer{"Bat Masterson", 2, true, AddressInfo{"777 Any St.", "Collins", "IA", "50055"}, CreditReport{Agency: CreditAgencyTransUnion, Report: AgencyReportTransUnionReport{TransUnionCreditReport{80}}}}),
+			},
+			kv{
+				"002001",
+				vdl.ValueOf(Invoice{2, 1001, 166, AddressInfo{"777 Any St.", "Collins", "IA", "50055"}}),
+			},
+			kv{
+				"002002",
+				vdl.ValueOf(Invoice{2, 1002, 243, AddressInfo{"888 Any St.", "Collins", "IA", "50055"}}),
+			},
+			kv{
+				"002003",
+				vdl.ValueOf(Invoice{2, 1004, 787, AddressInfo{"999 Any St.", "Collins", "IA", "50055"}}),
+			},
+			kv{
+				"002004",
+				vdl.ValueOf(Invoice{2, 1006, 88, AddressInfo{"101010 Any St.", "Collins", "IA", "50055"}}),
+			},
+		},
+	},
+	table{
+		name: "Numbers",
+		rows: []kv{
+			kv{
+				"001",
+				vdl.ValueOf(Numbers{byte(12), uint16(1234), uint32(5678), uint64(999888777666), int16(9876), int32(876543), int64(128), float32(3.14159), float64(2.71828182846), complex64(123.0 + 7.0i), complex128(456.789 + 10.1112i)}),
+			},
+			kv{
+				"002",
+				vdl.ValueOf(Numbers{byte(9), uint16(99), uint32(999), uint64(9999999), int16(9), int32(99), int64(88), float32(1.41421356237), float64(1.73205080757), complex64(9.87 + 7.65i), complex128(4.32 + 1.0i)}),
+			},
+			kv{
+				"003",
+				vdl.ValueOf(Numbers{byte(210), uint16(210), uint32(210), uint64(210), int16(210), int32(210), int64(210), float32(210.0), float64(210.0), complex64(210.0 + 0.0i), complex128(210.0 + 0.0i)}),
+			},
+		},
+	},
+	table{
+		name: "Composites",
+		rows: []kv{
+			kv{
+				"uno",
+				vdl.ValueOf(Composite{Array2String{"foo", "bar"}, []int32{1, 2}, map[int32]struct{}{1: struct{}{}, 2: struct{}{}}, map[string]int32{"foo": 1, "bar": 2}}),
+			},
+		},
+	},
+	table{
+		name: "Recursives",
+		rows: []kv{
+			kv{
+				"alpha",
+				vdl.ValueOf(Recursive{nil, &Times{time.Unix(123456789, 42244224), time.Duration(1337)}, map[Array2String]Recursive{
+					Array2String{"a", "b"}: Recursive{},
+					Array2String{"x", "y"}: Recursive{vdl.ValueOf(CreditReport{Agency: CreditAgencyExperian, Report: AgencyReportExperianReport{ExperianCreditReport{ExperianRatingGood}}}), nil, map[Array2String]Recursive{
+						Array2String{"alpha", "beta"}: Recursive{vdl.ValueOf(FooType{Bar: BarType{Baz: BazType{Name: "hello", TitleOrValue: TitleOrValueTypeValue{Value: 42}}}}), nil, nil},
+					}},
+					Array2String{"u", "v"}: Recursive{vdl.ValueOf(vdl.TypeOf(Recursive{})), nil, nil},
+				}}),
+			},
+		},
+	},
+}
+
+// Creates demo tables in the provided database. Tables are deleted and
+// recreated if they already exist.
+func PopulateDemoDB(ctx *context.T, db nosql.Database) error {
+	for i, t := range demoTables {
+		tn := demoPrefix + t.name
+		if err := db.DeleteTable(ctx, tn); err != nil {
+			return fmt.Errorf("failed deleting table %s (%d/%d): %v", tn, i+1, len(demoTables), err)
+		}
+		if err := db.CreateTable(ctx, tn, nil); err != nil {
+			return fmt.Errorf("failed creating table %s (%d/%d): %v", tn, i+1, len(demoTables), err)
+		}
+		if err := nosql.RunInBatch(ctx, db, wire.BatchOptions{}, func(db nosql.BatchDatabase) error {
+			dt := db.Table(tn)
+			for _, kv := range t.rows {
+				if err := dt.Put(ctx, kv.key, kv.value); err != nil {
+					return err
+				}
+			}
+			return nil
+		}); err != nil {
+			return fmt.Errorf("failed populating table %s (%d/%d): %v", tn, i+1, len(demoTables), err)
+		}
+	}
+	return nil
+}
diff --git a/v23/syncbase/nosql/internal/query/demo/db/db_objects.vdl b/x/ref/syncbase/sb51/internal/demodb/db_objects.vdl
similarity index 98%
rename from v23/syncbase/nosql/internal/query/demo/db/db_objects.vdl
rename to x/ref/syncbase/sb51/internal/demodb/db_objects.vdl
index 60e1004..cbf119a 100644
--- a/v23/syncbase/nosql/internal/query/demo/db/db_objects.vdl
+++ b/x/ref/syncbase/sb51/internal/demodb/db_objects.vdl
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package db
+package demodb
 
 import "time"
 
diff --git a/v23/syncbase/nosql/internal/query/demo/db/db_objects.vdl.go b/x/ref/syncbase/sb51/internal/demodb/db_objects.vdl.go
similarity index 82%
rename from v23/syncbase/nosql/internal/query/demo/db/db_objects.vdl.go
rename to x/ref/syncbase/sb51/internal/demodb/db_objects.vdl.go
index 66e4ad4..23481f8 100644
--- a/v23/syncbase/nosql/internal/query/demo/db/db_objects.vdl.go
+++ b/x/ref/syncbase/sb51/internal/demodb/db_objects.vdl.go
@@ -5,7 +5,7 @@
 // This file was auto-generated by the vanadium vdl tool.
 // Source: db_objects.vdl
 
-package db
+package demodb
 
 import (
 	// VDL system imports
@@ -25,7 +25,7 @@
 }
 
 func (AddressInfo) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.AddressInfo"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.AddressInfo"`
 }) {
 }
 
@@ -60,7 +60,7 @@
 		return nil
 	}
 	*x = -1
-	return fmt.Errorf("unknown label %q in db.CreditAgency", label)
+	return fmt.Errorf("unknown label %q in demodb.CreditAgency", label)
 }
 
 // String returns the string label of x.
@@ -77,7 +77,7 @@
 }
 
 func (CreditAgency) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.CreditAgency"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.CreditAgency"`
 	Enum struct{ Equifax, Experian, TransUnion string }
 }) {
 }
@@ -109,7 +109,7 @@
 		return nil
 	}
 	*x = -1
-	return fmt.Errorf("unknown label %q in db.ExperianRating", label)
+	return fmt.Errorf("unknown label %q in demodb.ExperianRating", label)
 }
 
 // String returns the string label of x.
@@ -124,7 +124,7 @@
 }
 
 func (ExperianRating) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.ExperianRating"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.ExperianRating"`
 	Enum struct{ Good, Bad string }
 }) {
 }
@@ -134,7 +134,7 @@
 }
 
 func (EquifaxCreditReport) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.EquifaxCreditReport"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.EquifaxCreditReport"`
 }) {
 }
 
@@ -143,7 +143,7 @@
 }
 
 func (ExperianCreditReport) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.ExperianCreditReport"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.ExperianCreditReport"`
 }) {
 }
 
@@ -152,7 +152,7 @@
 }
 
 func (TransUnionCreditReport) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.TransUnionCreditReport"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.TransUnionCreditReport"`
 }) {
 }
 
@@ -176,7 +176,7 @@
 	AgencyReportTransUnionReport struct{ Value TransUnionCreditReport }
 	// __AgencyReportReflect describes the AgencyReport union type.
 	__AgencyReportReflect struct {
-		Name  string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.AgencyReport"`
+		Name  string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.AgencyReport"`
 		Type  AgencyReport
 		Union struct {
 			EquifaxReport    AgencyReportEquifaxReport
@@ -207,7 +207,7 @@
 }
 
 func (CreditReport) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.CreditReport"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.CreditReport"`
 }) {
 }
 
@@ -220,7 +220,7 @@
 }
 
 func (Customer) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.Customer"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Customer"`
 }) {
 }
 
@@ -232,7 +232,7 @@
 }
 
 func (Invoice) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.Invoice"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Invoice"`
 }) {
 }
 
@@ -251,7 +251,7 @@
 }
 
 func (Numbers) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.Numbers"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Numbers"`
 }) {
 }
 
@@ -260,7 +260,7 @@
 }
 
 func (FooType) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.FooType"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.FooType"`
 }) {
 }
 
@@ -269,7 +269,7 @@
 }
 
 func (BarType) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.BarType"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.BarType"`
 }) {
 }
 
@@ -291,7 +291,7 @@
 	TitleOrValueTypeValue struct{ Value int64 }
 	// __TitleOrValueTypeReflect describes the TitleOrValueType union type.
 	__TitleOrValueTypeReflect struct {
-		Name  string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.TitleOrValueType"`
+		Name  string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.TitleOrValueType"`
 		Type  TitleOrValueType
 		Union struct {
 			Title TitleOrValueTypeTitle
@@ -316,14 +316,14 @@
 }
 
 func (BazType) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.BazType"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.BazType"`
 }) {
 }
 
 type Array2String [2]string
 
 func (Array2String) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.Array2String"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Array2String"`
 }) {
 }
 
@@ -335,7 +335,7 @@
 }
 
 func (Composite) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.Composite"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Composite"`
 }) {
 }
 
@@ -345,7 +345,7 @@
 }
 
 func (Times) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.Times"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Times"`
 }) {
 }
 
@@ -356,7 +356,7 @@
 }
 
 func (Recursive) __VDLReflect(struct {
-	Name string `vdl:"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db.Recursive"`
+	Name string `vdl:"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb.Recursive"`
 }) {
 }
 
diff --git a/x/ref/syncbase/sb51/internal/demodb/doc.go b/x/ref/syncbase/sb51/internal/demodb/doc.go
new file mode 100644
index 0000000..3daf52f
--- /dev/null
+++ b/x/ref/syncbase/sb51/internal/demodb/doc.go
@@ -0,0 +1,7 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package demodb supports loading an example database into Syncbase for
+// experimentation and testing purposes.
+package demodb
diff --git a/v23/syncbase/nosql/internal/query/demo/reader/reader.go b/x/ref/syncbase/sb51/internal/reader/reader.go
similarity index 95%
rename from v23/syncbase/nosql/internal/query/demo/reader/reader.go
rename to x/ref/syncbase/sb51/internal/reader/reader.go
index a50260a..930c140 100644
--- a/v23/syncbase/nosql/internal/query/demo/reader/reader.go
+++ b/x/ref/syncbase/sb51/internal/reader/reader.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package reader provides an object that read queries from various input
+// Package reader provides an object that reads queries from various input
 // sources (e.g. stdin, pipe).
 package reader
 
@@ -118,7 +118,7 @@
 }
 
 func (i *interactive) InitialPrompt() (string, error) {
-	return i.line.Prompt("Enter query or 'dump'? ")
+	return i.line.Prompt("? ")
 }
 
 func (i *interactive) ContinuePrompt() (string, error) {
diff --git a/v23/syncbase/nosql/internal/query/demo/reader/reader_test.go b/x/ref/syncbase/sb51/internal/reader/reader_test.go
similarity index 100%
rename from v23/syncbase/nosql/internal/query/demo/reader/reader_test.go
rename to x/ref/syncbase/sb51/internal/reader/reader_test.go
diff --git a/v23/syncbase/nosql/internal/query/demo/writer/doc.go b/x/ref/syncbase/sb51/internal/writer/doc.go
similarity index 82%
rename from v23/syncbase/nosql/internal/query/demo/writer/doc.go
rename to x/ref/syncbase/sb51/internal/writer/doc.go
index 327277e..315ba57 100644
--- a/v23/syncbase/nosql/internal/query/demo/writer/doc.go
+++ b/x/ref/syncbase/sb51/internal/writer/doc.go
@@ -3,4 +3,6 @@
 // license that can be found in the LICENSE file.
 
 // Package writer provides functions for formatting query results.
+//
+// TODO(ivanpi): Export as VDL formatter library.
 package writer
diff --git a/v23/syncbase/nosql/internal/query/demo/writer/writer.go b/x/ref/syncbase/sb51/internal/writer/writer.go
similarity index 97%
rename from v23/syncbase/nosql/internal/query/demo/writer/writer.go
rename to x/ref/syncbase/sb51/internal/writer/writer.go
index 4d04360..c7900b6 100644
--- a/v23/syncbase/nosql/internal/query/demo/writer/writer.go
+++ b/x/ref/syncbase/sb51/internal/writer/writer.go
@@ -13,7 +13,7 @@
 	"strings"
 	"unicode/utf8"
 
-	"v.io/syncbase/v23/syncbase/nosql/internal/query"
+	"v.io/syncbase/v23/syncbase/nosql"
 	"v.io/v23/vdl"
 	vtime "v.io/v23/vdlroot/time"
 )
@@ -27,7 +27,7 @@
 )
 
 // WriteTable formats the results as ASCII tables.
-func WriteTable(out io.Writer, columnNames []string, rs query.ResultStream) error {
+func WriteTable(out io.Writer, columnNames []string, rs nosql.ResultStream) error {
 	// Buffer the results so we can compute the column widths.
 	columnWidths := make([]int, len(columnNames))
 	for i, cName := range columnNames {
@@ -103,7 +103,7 @@
 }
 
 // WriteCSV formats the results as CSV as specified by https://tools.ietf.org/html/rfc4180.
-func WriteCSV(out io.Writer, columnNames []string, rs query.ResultStream, delimiter string) error {
+func WriteCSV(out io.Writer, columnNames []string, rs nosql.ResultStream, delimiter string) error {
 	delim := ""
 	for _, cName := range columnNames {
 		str := doubleQuoteForCSV(cName, delimiter)
@@ -140,7 +140,7 @@
 }
 
 // WriteJson formats the result as a JSON array of arrays (rows) of values.
-func WriteJson(out io.Writer, columnNames []string, rs query.ResultStream) error {
+func WriteJson(out io.Writer, columnNames []string, rs nosql.ResultStream) error {
 	io.WriteString(out, "[")
 	jsonColNames := make([][]byte, len(columnNames))
 	for i, cName := range columnNames {
diff --git a/v23/syncbase/nosql/internal/query/demo/writer/writer_test.go b/x/ref/syncbase/sb51/internal/writer/writer_test.go
similarity index 98%
rename from v23/syncbase/nosql/internal/query/demo/writer/writer_test.go
rename to x/ref/syncbase/sb51/internal/writer/writer_test.go
index 55a3e00..d19b43f 100644
--- a/v23/syncbase/nosql/internal/query/demo/writer/writer_test.go
+++ b/x/ref/syncbase/sb51/internal/writer/writer_test.go
@@ -10,9 +10,9 @@
 	"testing"
 	"time"
 
-	"v.io/syncbase/v23/syncbase/nosql/internal/query"
-	"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/db"
-	"v.io/syncbase/v23/syncbase/nosql/internal/query/demo/writer"
+	"v.io/syncbase/v23/syncbase/nosql"
+	db "v.io/syncbase/x/ref/syncbase/sb51/internal/demodb"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/writer"
 	"v.io/v23/vdl"
 )
 
@@ -21,7 +21,7 @@
 	curr int
 }
 
-func newResultStream(iRows [][]interface{}) query.ResultStream {
+func newResultStream(iRows [][]interface{}) nosql.ResultStream {
 	vRows := make([][]*vdl.Value, len(iRows))
 	for i, iRow := range iRows {
 		vRow := make([]*vdl.Value, len(iRow))
diff --git a/x/ref/syncbase/sb51/main.go b/x/ref/syncbase/sb51/main.go
new file mode 100644
index 0000000..9968722
--- /dev/null
+++ b/x/ref/syncbase/sb51/main.go
@@ -0,0 +1,34 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Antimony (sb51) - Syncbase general-purpose client and management utility.
+// Currently supports SyncQL select queries.
+
+package main
+
+import (
+	"flag"
+
+	"v.io/x/lib/cmdline"
+	_ "v.io/x/ref/runtime/factories/generic"
+)
+
+func main() {
+	cmdline.Main(cmdSb51)
+}
+
+var cmdSb51 = &cmdline.Command{
+	Name:  "sb51",
+	Short: "Antimony - Vanadium Syncbase client and management utility",
+	Long: `
+Syncbase general-purpose client and management utility.
+Currently supports starting a SyncQL shell.
+`,
+	Children: []*cmdline.Command{cmdSbShell},
+}
+
+var (
+	// TODO(ivanpi): Decide on convention for local syncbase service name.
+	flagSbService = flag.String("service", "/:8101/syncbase", "Location of the Syncbase service to connect to. Can be absolute or relative to the namespace root.")
+)
diff --git a/x/ref/syncbase/sb51/shell.go b/x/ref/syncbase/sb51/shell.go
new file mode 100644
index 0000000..a41d245
--- /dev/null
+++ b/x/ref/syncbase/sb51/shell.go
@@ -0,0 +1,240 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Syncbase client shell. Currently supports SyncQL select queries.
+
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+	"strings"
+
+	isatty "github.com/mattn/go-isatty"
+
+	"v.io/syncbase/v23/syncbase"
+	"v.io/syncbase/v23/syncbase/nosql"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/demodb"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/reader"
+	"v.io/syncbase/x/ref/syncbase/sb51/internal/writer"
+	"v.io/v23/context"
+	"v.io/x/lib/cmdline"
+	"v.io/x/ref/lib/v23cmd"
+)
+
+var cmdSbShell = &cmdline.Command{
+	Runner: v23cmd.RunnerFunc(runSbShell),
+	Name:   "sh",
+	Short:  "Start a SyncQL shell",
+	Long: `
+Connect to a database on the Syncbase service and start a SyncQL shell.
+`,
+	ArgsName: "<app_name> <db_name>",
+	ArgsLong: `
+<app_name> and <db_name> specify the database to execute queries against.
+The database must exist unless -create-missing is specified.
+`,
+}
+
+var (
+	flagFormat            string
+	flagCSVDelimiter      string
+	flagCreateIfNotExists bool
+	flagMakeDemoTables    bool
+)
+
+func init() {
+	cmdSbShell.Flags.StringVar(&flagFormat, "format", "table", "Output format. 'table': human-readable table; 'csv': comma-separated values, use -csv-delimiter to control the delimiter; 'json': JSON objects.")
+	cmdSbShell.Flags.StringVar(&flagCSVDelimiter, "csv-delimiter", ",", "Delimiter to use when printing data as CSV (e.g. \"\t\", \",\")")
+	cmdSbShell.Flags.BoolVar(&flagCreateIfNotExists, "create-missing", false, "Create the app and/or database if they do not exist yet.")
+	cmdSbShell.Flags.BoolVar(&flagMakeDemoTables, "make-demo", false, "(Re)create demo tables in the database.")
+}
+
+func validateFlags() error {
+	if flagFormat != "table" && flagFormat != "csv" && flagFormat != "json" {
+		return fmt.Errorf("Unsupported -format %q.  Must be one of 'table', 'csv', or 'json'.", flagFormat)
+	}
+	if len(flagCSVDelimiter) == 0 {
+		return fmt.Errorf("-csv-delimiter cannot be empty.")
+	}
+	return nil
+}
+
+// Starts a SyncQL shell against the specified database.
+// Runs in interactive or batch mode depending on stdin.
+func runSbShell(ctx *context.T, env *cmdline.Env, args []string) error {
+	// TODO(ivanpi): Add 'use' statement, default to no app/database selected.
+	if len(args) != 2 {
+		return env.UsageErrorf("exactly two arguments expected")
+	}
+	appName, dbName := args[0], args[1]
+	if err := validateFlags(); err != nil {
+		return env.UsageErrorf("%v", err)
+	}
+
+	sbs := syncbase.NewService(*flagSbService)
+	d, err := openAppDB(ctx, sbs, appName, dbName, flagCreateIfNotExists)
+	if err != nil {
+		return err
+	}
+
+	if flagMakeDemoTables {
+		if err := makeDemoDB(ctx, d); err != nil {
+			return err
+		}
+	}
+
+	var input *reader.T
+	// TODO(ivanpi): This is hacky, it would be better for lib/cmdline to support IsTerminal.
+	stdinFile, ok := env.Stdin.(*os.File)
+	isTerminal := ok && isatty.IsTerminal(stdinFile.Fd())
+	if isTerminal {
+		input = reader.NewInteractive()
+	} else {
+		input = reader.NewNonInteractive()
+	}
+	defer input.Close()
+
+stmtLoop:
+	for true {
+		if q, err := input.GetQuery(); err != nil {
+			if err == io.EOF {
+				if isTerminal {
+					// ctrl-d
+					fmt.Println()
+				}
+				break
+			} else {
+				// ctrl-c
+				break
+			}
+		} else {
+			var err error
+			tq := strings.Fields(q)
+			if len(tq) > 0 {
+				switch strings.ToLower(tq[0]) {
+				case "exit", "quit":
+					break stmtLoop
+				case "dump":
+					err = dumpDB(ctx, env.Stdout, d)
+				case "make-demo":
+					err = makeDemoDB(ctx, d)
+				case "select":
+					err = queryExec(ctx, env.Stdout, d, q)
+				default:
+					err = fmt.Errorf("unknown statement: '%s'; expected one of: 'select', 'make-demo', 'dump', 'exit', 'quit'", strings.ToLower(tq[0]))
+				}
+			}
+			if err != nil {
+				if isTerminal {
+					fmt.Fprintln(env.Stderr, "Error:", err)
+				} else {
+					// If running non-interactively, errors stop execution.
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func openAppDB(ctx *context.T, sbs syncbase.Service, appName, dbName string, createIfNotExists bool) (nosql.Database, error) {
+	app := sbs.App(appName)
+	if exists, err := app.Exists(ctx); err != nil {
+		return nil, fmt.Errorf("failed checking for app %q: %v", app.FullName(), err)
+	} else if !exists {
+		if !createIfNotExists {
+			return nil, fmt.Errorf("app %q does not exist", app.FullName())
+		}
+		if err := app.Create(ctx, nil); err != nil {
+			return nil, err
+		}
+	}
+	d := app.NoSQLDatabase(dbName)
+	if exists, err := d.Exists(ctx); err != nil {
+		return nil, fmt.Errorf("failed checking for db %q: %v", d.FullName(), err)
+	} else if !exists {
+		if !createIfNotExists {
+			return nil, fmt.Errorf("db %q does not exist", d.FullName())
+		}
+		if err := d.Create(ctx, nil); err != nil {
+			return nil, err
+		}
+	}
+	return d, nil
+}
+
+func dumpDB(ctx *context.T, w io.Writer, d nosql.Database) error {
+	tables, err := d.ListTables(ctx)
+	if err != nil {
+		return fmt.Errorf("failed listing tables: %v", err)
+	}
+	var errs []error
+	for _, table := range tables {
+		fmt.Fprintf(w, "table: %s\n", table)
+		if err := queryExec(ctx, w, d, fmt.Sprintf("select k, v from %s", table)); err != nil {
+			errs = append(errs, fmt.Errorf("> %s: %v", table, err))
+		}
+	}
+	if len(errs) > 0 {
+		err := fmt.Errorf("failed dumping %d of %d tables:", len(errs), len(tables))
+		for _, e := range errs {
+			err = fmt.Errorf("%v\n%v", err, e)
+		}
+		return err
+	}
+	return nil
+}
+
+func makeDemoDB(ctx *context.T, d nosql.Database) error {
+	if err := demodb.PopulateDemoDB(ctx, d); err != nil {
+		return fmt.Errorf("failed making demo tables: %v", err)
+	}
+	return nil
+}
+
+// Split an error message into an offset and the remaining (i.e., rhs of offset) message.
+// The convention for syncql is "<module><optional-rpc>[offset]<remaining-message>".
+func splitError(err error) (int64, string) {
+	errMsg := err.Error()
+	idx1 := strings.Index(errMsg, "[")
+	idx2 := strings.Index(errMsg, "]")
+	if idx1 == -1 || idx2 == -1 {
+		return 0, errMsg
+	}
+	offsetString := errMsg[idx1+1 : idx2]
+	offset, err := strconv.ParseInt(offsetString, 10, 64)
+	if err != nil {
+		return 0, errMsg
+	}
+	return offset, errMsg[idx2+1:]
+}
+
+func queryExec(ctx *context.T, w io.Writer, d nosql.Database, q string) error {
+	if columnNames, rs, err := d.Exec(ctx, q); err != nil {
+		off, msg := splitError(err)
+		return fmt.Errorf("\n%s\n%s^\n%d: %s", q, strings.Repeat(" ", int(off)), off+1, msg)
+	} else {
+		switch flagFormat {
+		case "table":
+			if err := writer.WriteTable(w, columnNames, rs); err != nil {
+				return err
+			}
+		case "csv":
+			if err := writer.WriteCSV(w, columnNames, rs, flagCSVDelimiter); err != nil {
+				return err
+			}
+		case "json":
+			if err := writer.WriteJson(w, columnNames, rs); err != nil {
+				return err
+			}
+		default:
+			panic(fmt.Sprintf("invalid format flag value: %v", flagFormat))
+		}
+	}
+	return nil
+}