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 caadf1f.
Reproducer
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:
$ SPANNER_EMULATOR_HOST=localhost:9010 go run .
Actual result
StatementParser.Split:
single with semicolon: multiple=false statements=[]string(nil) err=<nil>
single without semicolon: multiple=false statements=[]string(nil) err=<nil>
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=<nil>
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: <nil>
multiple: <nil>
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.
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.Splitresults for those inputs.Environment
github.com/googleapis/go-sql-spannerv1.26.0The underlying
StatementParser.Splitbehavior was also verified at the currentmaincommit caadf1f.Reproducer
Run against a fresh Emulator with:
$ SPANNER_EMULATOR_HOST=localhost:9010 go run .Actual result
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.QueryContextcallsStatementParser.Split. For exactly one statement,including one terminated by a semicolon,
Splitreturnsfalse, nil, nilbydesign. 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,
Splitreturns 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 alsoaffects direct consumers using
Splitfor those use cases: unlike multiplestatements, a separator-free statement is not returned for a single statement
unless they compensate for it.