Environment details
- Programming language: Go
- OS: macOS (Darwin/arm64)
- Language runtime version: Go 1.26.5
- Package version:
github.com/googleapis/go-sql-spanner v1.26.0
- Cloud Spanner Emulator: v1.5.56
- Database dialect: GoogleSQL
Steps to reproduce
- Start a fresh Cloud Spanner Emulator.
- Save the following as
main.go and run it with github.com/googleapis/go-sql-spanner v1.26.0:
package main
import (
"context"
"database/sql"
"fmt"
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
_ "github.com/googleapis/go-sql-spanner"
"github.com/googleapis/go-sql-spanner/parser"
)
func main() {
p, err := parser.NewStatementParser(databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL, 0)
if err != nil {
panic(err)
}
for _, q := range []string{"-- comment only\n", "NOT A STATEMENT"} {
fmt.Printf("%q: %v\n", q, p.DetectStatementType(q).StatementType)
}
db, err := sql.Open("spanner", "projects/test-project/instances/test-instance/databases/test-database?autoConfigEmulator=true")
if err != nil {
panic(err)
}
defer db.Close()
for _, q := range []string{"-- comment only\n", "SELECT 1;;SELECT 2"} {
fmt.Printf("%q: %v\n", q, query(context.Background(), db, q))
}
}
func query(ctx context.Context, db *sql.DB, q string) error {
rows, err := db.QueryContext(ctx, q)
if err != nil {
return err
}
defer rows.Close()
for {
for rows.Next() {
}
if err := rows.Err(); err != nil {
return err
}
if !rows.NextResultSet() {
return rows.Err()
}
}
}
Actual behavior
DetectStatementType reports both comment-only input and invalid SQL as StatementTypeUnknown. QueryContext sends comment-only input to Spanner, where it fails with InvalidArgument: Unexpected end of statement. In SELECT 1;;SELECT 2, the first query executes, the empty statement fails, and the second query is not reached. Because later errors are reported through Rows.Err, QueryContext itself can return without an error.
This is more consequential for DDL. For example, in CREATE TABLE T1 (...);;CREATE TABLE T2 (...), T1 can be applied before the empty fragment fails and prevents T2 from running. The preceding DDL is not rolled back.
Expected behavior
There is no universal cross-database contract for a wholly empty query. This issue is limited to empty or comment-only fragments encountered while handling multi-statement input.
QueryContext should handle those fragments consistently before executing any non-empty fragment instead of forwarding them to Spanner partway through iteration. Discarding them, as PostgreSQL's top-level parser does, is one reasonable behavior; rejecting the complete input before execution would also avoid the current partial-processing behavior. Non-empty invalid SQL must remain distinguishable.
Cause
The simple parser retains empty fragments when splitting statements, while DetectStatementType classifies both empty/comment-only input and non-empty unrecognized SQL as StatementTypeUnknown. Callers therefore cannot distinguish SQL trivia from an invalid statement.
QueryContext treats these unknown fragments as queries and sends them to Spanner. A parser API that identifies whether input contains no statement tokens would let QueryContext handle empty/comment-only fragments without hiding invalid SQL.
References
The empty and multi-statement cases above were also reproduced with PostgreSQL 18.4, MySQL 8.4.11, and SQLite 3.51.0.
Relation to #461
This behavior affects the simple-parser use cases described in #461.
Environment details
github.com/googleapis/go-sql-spannerv1.26.0Steps to reproduce
main.goand run it withhub.lumenfield.work/googleapis/go-sql-spannerv1.26.0:Actual behavior
DetectStatementTypereports both comment-only input and invalid SQL asStatementTypeUnknown.QueryContextsends comment-only input to Spanner, where it fails withInvalidArgument: Unexpected end of statement. InSELECT 1;;SELECT 2, the first query executes, the empty statement fails, and the second query is not reached. Because later errors are reported throughRows.Err,QueryContextitself can return without an error.This is more consequential for DDL. For example, in
CREATE TABLE T1 (...);;CREATE TABLE T2 (...),T1can be applied before the empty fragment fails and preventsT2from running. The preceding DDL is not rolled back.Expected behavior
There is no universal cross-database contract for a wholly empty query. This issue is limited to empty or comment-only fragments encountered while handling multi-statement input.
QueryContextshould handle those fragments consistently before executing any non-empty fragment instead of forwarding them to Spanner partway through iteration. Discarding them, as PostgreSQL's top-level parser does, is one reasonable behavior; rejecting the complete input before execution would also avoid the current partial-processing behavior. Non-empty invalid SQL must remain distinguishable.Cause
The simple parser retains empty fragments when splitting statements, while
DetectStatementTypeclassifies both empty/comment-only input and non-empty unrecognized SQL asStatementTypeUnknown. Callers therefore cannot distinguish SQL trivia from an invalid statement.QueryContexttreats these unknown fragments as queries and sends them to Spanner. A parser API that identifies whether input contains no statement tokens would letQueryContexthandle empty/comment-only fragments without hiding invalid SQL.References
database/sql.QueryContextdocumentation does not define empty-query or multi-statement behavior.EmptyQueryResponsefor a wholly empty query string. PostgreSQL comments are equivalent to whitespace.ER_EMPTY_QUERY(Query was empty).The empty and multi-statement cases above were also reproduced with PostgreSQL 18.4, MySQL 8.4.11, and SQLite 3.51.0.
Relation to #461
This behavior affects the simple-parser use cases described in #461.