diff --git a/agents/drivers/xugu/main.go b/agents/drivers/xugu/main.go index a62123382..8a0b4b807 100644 --- a/agents/drivers/xugu/main.go +++ b/agents/drivers/xugu/main.go @@ -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 { diff --git a/agents/drivers/xugu/main_test.go b/agents/drivers/xugu/main_test.go index 82079a9a8..7f1a18ee3 100644 --- a/agents/drivers/xugu/main_test.go +++ b/agents/drivers/xugu/main_test.go @@ -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{}