googleapis / googleapis/go-sql-spanner

Single DDL terminated by a semicolon fails through `QueryContext`, while multiple DDL succeeds

Open
#891 1 comment 0 reactions 0 assignees View on GitHub
priority: p2 type: bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.