googleapis / googleapis/go-sql-spanner
Single DDL terminated by a semicolon fails through `QueryContext`, while multiple DDL succeeds
- Dominant language
- Go
- Stars
- 125
- Forks
- 35
- Avg merge
- 2h 20m
- Merged PRs (30d)
- 4
Description
Using `database/sql`, a single DDL statement with a terminating semicolon fails,
while the same form without the semicolon and a multi-statement DDL string both
succeed. The same reproducer also prints the underlying
`StatementParser.Split` results for those inputs.
## Environment
- `github.com/googleapis/go-sql-spanner` v1.26.0
- Cloud Spanner Emulator v1.5.56
- GoogleSQL dialect
The underlying `StatementParser.Split` behavior was also verified at the current
`main` commit caadf1f9c2abd602bcbe0fc2c85e7bcdd771df08.
## Reproducer
```go
package main
import (
"context"
"database/sql"
"fmt"
"log"
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
_ "github.com/googleapis/go-sql-spanner"
"github.com/googleapis/go-sql-spanner/parser"
)
func main() {
const (
singleWithSemicolon = "CREATE TABLE SingleWithSemicolon (Id INT64 NOT NULL) PRIMARY KEY (Id);"
singleWithoutSemicolon = "CREATE TABLE SingleWithoutSemicolon (Id INT64 NOT NULL) " +
"PRIMARY KEY (Id)"
multiple = "CREATE TABLE MultiOne (Id INT64 NOT NULL) PRIMARY KEY (Id);" +
"CREATE TABLE MultiTwo (Id INT64 NOT NULL) PRIMARY KEY (Id);"
)
p, err := parser.NewStatementParser(databasepb.DatabaseDialect_GOOGLE_STANDARD_SQL, 0)
if err != nil {
log.Fatal(err)
}
fmt.Println("StatementParser.Split:")
printSplit(p, "single with semicolon", singleWithSemicolon)
printSplit(p, "single without semicolon", singleWithoutSemicolon)
printSplit(p, "multiple", multiple)
ctx := context.Background()
db, err := sql.Open(
"spanner",
"projects/emulator-project/instances/test-instance/databases/test-database;autoConfigEmulator=true",
)
if err != nil {
log.Fatal(err)
}
defer db.Close()
fmt.Println("\ndatabase/sql.QueryContext:")
fmt.Printf("single with semicolon: %v\n", query(ctx, db, singleWithSemicolon))
fmt.Printf("single without semicolon: %v\n", query(ctx, db, singleWithoutSemicolon))
fmt.Printf("multiple: %v\n", query(ctx, db, multiple))
}
func printSplit(p *parser.StatementParser, label, statement string) {
multiple, statements, err := p.Split(statement)
fmt.Printf("%s: multiple=%v statements=%#v err=%v\n", label, multiple, statements, err)
}
func query(ctx context.Context, db *sql.DB, statement string) error {
rows, err := db.QueryContext(ctx, statement)
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()
}
}
}
```
Run against a fresh Emulator with:
```console
$ SPANNER_EMULATOR_HOST=localhost:9010 go run .
```
## Actual result
```text
StatementParser.Split:
single with semicolon: multiple=false statements=[]string(nil) err=
single without semicolon: multiple=false statements=[]string(nil) err=
multiple: multiple=true statements=[]string{"CREATE TABLE MultiOne (Id INT64 NOT NULL) PRIMARY KEY (Id)", "CREATE TABLE MultiTwo (Id INT64 NOT NULL) PRIMARY KEY (Id)"} err=
database/sql.QueryContext:
single with semicolon: rpc error: code = InvalidArgument desc = Error parsing Spanner DDL statement: CREATE TABLE SingleWithSemicolon (Id INT64 NOT NULL) PRIMARY KEY (Id); : Syntax error on line 1, column 70: Expecting 'EOF' but found an unknown character (';').
single without semicolon:
multiple:
```
## Expected result
The single DDL statement with a separator semicolon should succeed in the same
way as the separator-free single statement and the statements in the
multi-statement string.
## Cause
`conn.QueryContext` calls `StatementParser.Split`. For exactly one statement,
including one terminated by a semicolon, `Split` returns `false, nil, nil` by
design. The driver therefore takes its single-statement path with the original
SQL, leaving the separator semicolon in the DDL sent to `UpdateDatabaseDdl`.
For multiple statements, `Split` returns each statement without its separator,
so the same DDL form succeeds.
## Relation to #461
Issue #461 described the need for an exported simple statement parser,
including statement splitting for tools such as `wrench`. This behavior also
affects direct consumers using `Split` for those use cases: unlike multiple
statements, a separator-free statement is not returned for a single statement
unless they compensate for it.
Contributor guide
Research direction
Start by running the provided Go reproducer against a fresh Cloud Spanner Emulator, then inspect conn.QueryContext and parser.StatementParser.Split. Trace the single-statement path through the call to UpdateDatabaseDdl and compare it with the multi-statement path. Done means a single DDL statement ending in a semicolon succeeds like the other two cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, google-cloud
- Domain
- database
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100