fix(xugu): return SHOW statement result sets
This commit is contained in:
parent
e3361ad928
commit
7e024b8cef
|
|
@ -16,6 +16,8 @@ import (
|
|||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
_ "gitee.com/XuguDB/go-xugu-driver"
|
||||
)
|
||||
|
|
@ -3953,8 +3955,28 @@ func stripLeadingSQLComments(sqlText string) string {
|
|||
}
|
||||
|
||||
func isQuerySQL(sqlText string) bool {
|
||||
lower := strings.ToLower(strings.TrimSpace(sqlText))
|
||||
return strings.HasPrefix(lower, "select") || strings.HasPrefix(lower, "with")
|
||||
sqlText = stripLeadingSQLComments(sqlText)
|
||||
for _, keyword := range []string{"select", "with", "show"} {
|
||||
if hasLeadingSQLKeyword(sqlText, keyword) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasLeadingSQLKeyword(sqlText, keyword string) bool {
|
||||
if len(sqlText) < len(keyword) || !strings.EqualFold(sqlText[:len(keyword)], keyword) {
|
||||
return false
|
||||
}
|
||||
if len(sqlText) == len(keyword) {
|
||||
return true
|
||||
}
|
||||
next, _ := utf8.DecodeRuneInString(sqlText[len(keyword):])
|
||||
return !isSQLIdentifierContinuation(next)
|
||||
}
|
||||
|
||||
func isSQLIdentifierContinuation(value rune) bool {
|
||||
return value == '_' || value == '$' || value == '#' || unicode.IsLetter(value) || unicode.IsDigit(value) || unicode.IsMark(value)
|
||||
}
|
||||
|
||||
func quoteIdentifier(value string) string {
|
||||
|
|
|
|||
|
|
@ -1472,6 +1472,116 @@ func TestExecuteQueryPreservesXuguTypeBodyTerminator(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestXuguShowStatementsUseResultSetQueryPath(t *testing.T) {
|
||||
resetXuguShowResultDriver()
|
||||
db, err := sql.Open("xugu-test-show-result", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
s := newServer()
|
||||
s.db = db
|
||||
|
||||
result, err := s.executeQuery(queryOptions{SQL: "SHOW DB_INFO;"})
|
||||
if err != nil {
|
||||
t.Fatalf("executeQuery(SHOW DB_INFO): %v", err)
|
||||
}
|
||||
if got, want := result.Columns, []string{"DB_NAME", "DB_ID", "DB_OWNER", "DB_CHARSET", "DB_TIMEZ"}; !equalStrings(got, want) {
|
||||
t.Fatalf("SHOW DB_INFO columns = %v, want %v", got, want)
|
||||
}
|
||||
if len(result.Rows) != 1 || result.Rows[0][0] != "SYSTEM" {
|
||||
t.Fatalf("SHOW DB_INFO rows = %#v, want database row", result.Rows)
|
||||
}
|
||||
|
||||
page, err := s.executeQueryPage(queryOptions{SQL: "SHOW DB_INFO"}, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("executeQueryPage(SHOW DB_INFO): %v", err)
|
||||
}
|
||||
if len(page.Rows) != 1 || page.Rows[0][0] != "SYSTEM" {
|
||||
t.Fatalf("SHOW DB_INFO page rows = %#v, want database row", page.Rows)
|
||||
}
|
||||
|
||||
queries, execs := recordedXuguShowStatements()
|
||||
if got, want := queries, []string{"SHOW DB_INFO", "SHOW DB_INFO"}; !equalStrings(got, want) {
|
||||
t.Fatalf("SHOW statements queried = %v, want %v", got, want)
|
||||
}
|
||||
if len(execs) != 0 {
|
||||
t.Fatalf("SHOW statements must not use ExecContext, got %v", execs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXuguQueryKeywordBoundariesUseResultSetPath(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
sqlText string
|
||||
wantQuery string
|
||||
wantColumns []string
|
||||
wantValue any
|
||||
}{
|
||||
{name: "parenthesized select", sqlText: "SELECT(1);", wantQuery: "SELECT(1)", wantColumns: []string{"VALUE"}, wantValue: int64(1)},
|
||||
{name: "select hint", sqlText: "SELECT/*+ index */1;", wantQuery: "SELECT/*+ index */1", wantColumns: []string{"VALUE"}, wantValue: int64(1)},
|
||||
{name: "show comment", sqlText: "SHOW/* metadata */ DB_INFO;", wantQuery: "SHOW/* metadata */ DB_INFO", wantColumns: []string{"DB_NAME", "DB_ID", "DB_OWNER", "DB_CHARSET", "DB_TIMEZ"}, wantValue: "SYSTEM"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resetXuguShowResultDriver()
|
||||
db, err := sql.Open("xugu-test-show-result", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
s := newServer()
|
||||
s.db = db
|
||||
result, err := s.executeQuery(queryOptions{SQL: test.sqlText})
|
||||
if err != nil {
|
||||
t.Fatalf("executeQuery(%q): %v", test.sqlText, err)
|
||||
}
|
||||
if !equalStrings(result.Columns, test.wantColumns) {
|
||||
t.Fatalf("columns = %v, want %v", result.Columns, test.wantColumns)
|
||||
}
|
||||
if len(result.Rows) != 1 || len(result.Rows[0]) == 0 || result.Rows[0][0] != test.wantValue {
|
||||
t.Fatalf("rows = %#v, want first value %#v", result.Rows, test.wantValue)
|
||||
}
|
||||
|
||||
queries, execs := recordedXuguShowStatements()
|
||||
if !equalStrings(queries, []string{test.wantQuery}) {
|
||||
t.Fatalf("queries = %v, want %v", queries, []string{test.wantQuery})
|
||||
}
|
||||
if len(execs) != 0 {
|
||||
t.Fatalf("query statements must not use ExecContext, got %v", execs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsQuerySQLRecognizesQueryKeywordBoundaries(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
sqlText string
|
||||
want bool
|
||||
}{
|
||||
{sqlText: "SELECT 1", want: true},
|
||||
{sqlText: "SELECT(1)", want: true},
|
||||
{sqlText: "SELECT/*+ index */1", want: true},
|
||||
{sqlText: "WITH value AS (SELECT 1) SELECT * FROM value", want: true},
|
||||
{sqlText: "SHOW DB_INFO", want: true},
|
||||
{sqlText: " show current_schema", want: true},
|
||||
{sqlText: "/* Xugu metadata */ SHOW CHARSETS", want: true},
|
||||
{sqlText: "SHOW/* metadata */ DB_INFO", want: true},
|
||||
{sqlText: "-- leading comment\nSELECT(1)", want: true},
|
||||
{sqlText: "SELECTIVE settings", want: false},
|
||||
{sqlText: "SHOWCASE settings", want: false},
|
||||
{sqlText: "SHOW_CURRENT_SCHEMA", want: false},
|
||||
{sqlText: "CREATE TABLE items (id INTEGER)", want: false},
|
||||
} {
|
||||
t.Run(test.sqlText, func(t *testing.T) {
|
||||
if got := isQuerySQL(test.sqlText); got != test.want {
|
||||
t.Fatalf("isQuerySQL(%q) = %t, want %t", test.sqlText, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contains(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
|
|
@ -1481,6 +1591,18 @@ func contains(values []string, target string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func equalStrings(got, want []string) bool {
|
||||
if len(got) != len(want) {
|
||||
return false
|
||||
}
|
||||
for index := range got {
|
||||
if got[index] != want[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// -- fake drivers for agent tests --
|
||||
|
||||
func init() {
|
||||
|
|
@ -1490,6 +1612,63 @@ func init() {
|
|||
sql.Register("xugu-test-legacy-columns", &xuguLegacyColumnsDriver{})
|
||||
sql.Register("xugu-test-table-objects", &xuguTableObjectsDriver{})
|
||||
sql.Register("xugu-test-table-ddl", &xuguTableDDLDriver{})
|
||||
sql.Register("xugu-test-show-result", &xuguShowResultDriver{})
|
||||
}
|
||||
|
||||
type xuguShowResultDriver struct{}
|
||||
|
||||
var xuguShowResultState struct {
|
||||
sync.Mutex
|
||||
queries []string
|
||||
execs []string
|
||||
}
|
||||
|
||||
func resetXuguShowResultDriver() {
|
||||
xuguShowResultState.Lock()
|
||||
xuguShowResultState.queries = nil
|
||||
xuguShowResultState.execs = nil
|
||||
xuguShowResultState.Unlock()
|
||||
}
|
||||
|
||||
func recordedXuguShowStatements() (queries []string, execs []string) {
|
||||
xuguShowResultState.Lock()
|
||||
defer xuguShowResultState.Unlock()
|
||||
return append([]string(nil), xuguShowResultState.queries...), append([]string(nil), xuguShowResultState.execs...)
|
||||
}
|
||||
|
||||
func (d *xuguShowResultDriver) Open(name string) (driver.Conn, error) {
|
||||
return &xuguShowResultConn{}, nil
|
||||
}
|
||||
|
||||
type xuguShowResultConn struct{}
|
||||
|
||||
func (c *xuguShowResultConn) Prepare(query string) (driver.Stmt, error) {
|
||||
return nil, errors.New("not supported")
|
||||
}
|
||||
func (c *xuguShowResultConn) Close() error { return nil }
|
||||
func (c *xuguShowResultConn) Begin() (driver.Tx, error) { return nil, errors.New("not supported") }
|
||||
func (c *xuguShowResultConn) QueryContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Rows, error) {
|
||||
xuguShowResultState.Lock()
|
||||
xuguShowResultState.queries = append(xuguShowResultState.queries, query)
|
||||
xuguShowResultState.Unlock()
|
||||
|
||||
switch query {
|
||||
case "SELECT(1)", "SELECT/*+ index */1":
|
||||
return &xuguStaticRows{columns: []string{"VALUE"}, values: [][]driver.Value{{int64(1)}}}, nil
|
||||
case "SHOW DB_INFO", "SHOW/* metadata */ DB_INFO":
|
||||
return &xuguStaticRows{
|
||||
columns: []string{"DB_NAME", "DB_ID", "DB_OWNER", "DB_CHARSET", "DB_TIMEZ"},
|
||||
values: [][]driver.Value{{"SYSTEM", int64(1), "SYS", "UTF8.UTF8_GENERAL_CI", "GMT+08:00"}},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected query: %s", query)
|
||||
}
|
||||
}
|
||||
func (c *xuguShowResultConn) ExecContext(_ context.Context, query string, _ []driver.NamedValue) (driver.Result, error) {
|
||||
xuguShowResultState.Lock()
|
||||
xuguShowResultState.execs = append(xuguShowResultState.execs, query)
|
||||
xuguShowResultState.Unlock()
|
||||
return nil, fmt.Errorf("SHOW statement was incorrectly sent to ExecContext: %s", query)
|
||||
}
|
||||
|
||||
type xuguTableDDLDriver struct{}
|
||||
|
|
|
|||
Loading…
Reference in New Issue