Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,16 @@ Any pull request modifying or extending the driver's features must include:
- **Mock Server Tests**: Located in files such as `driver_with_mockserver_test.go`, `conn_with_mockserver_test.go`, and `stmt_with_mockserver_test.go`. Use these (or add new test files) to mock Spanner gRPC API responses (e.g. BeginTransaction, Commit, ExecuteSql) and verify that the driver translates options, tags, and states correctly.
- **Emulator Tests**: Validate integration behavior against the Cloud Spanner Emulator (`integration_test.go` and examples). Make sure the test configurations can run locally with `auto_config_emulator=true`.
- **Wrapper Tests**: If you modified `spannerlib`, ensure you trigger or run unit/integration tests for the respective wrappers (`python-spanner-lib-wrapper-unit-tests.yml`, `ruby-wrapper-tests.yml`, etc.).
- **Assertion Formatting**: When writing test assertions, strongly prefer using variable names `g` (got) and `w` (want) for comparison, and format error messages using the following aligned layout:
```go
if g, w := actualValue, expectedValue; g != w {
t.Errorf("some message mismatch\nGot: %v\nWant: %v", g, w)
}
```
Note the two spaces after `Got:` to align the values visually.

---

## 6. Code Style & Formatting

- **Go Code Formatting**: All Go code must be formatted using the standard `gofmt -w -s .` formatter. Running this formatting command is required before submitting a pull request.
39 changes: 1 addition & 38 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"database/sql"
"database/sql/driver"
"errors"
"fmt"
"log/slog"
"slices"
"sync"
Expand Down Expand Up @@ -705,43 +704,7 @@ func (c *conn) execDDL(ctx context.Context, statements ...spanner.Statement) (dr
return (&executableDropDatabaseStatement{stmt}).execContext(ctx, c, nil, []driver.NamedValue{})
}

op, err := c.adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: c.database,
Statements: ddlStatements,
})
if err != nil {
return nil, err
}
c.lastDDLOperationID = op.Name()

if err := c.waitForDDLOperation(ctx, op.Name(), func(ctx context.Context) error {
return op.Wait(ctx)
}); err != nil {
if len(statements) > 1 {
be := &BatchError{
Err: err,
BatchUpdateCounts: []int64{},
}
metadata, err := op.Metadata()
if err != nil {
c.logger.WarnContext(ctx, fmt.Sprintf("Error getting metadata for UpdateDatabaseDdl: %v", err))
} else if metadata != nil {
for _, ts := range metadata.CommitTimestamps {
if ts != nil {
be.BatchUpdateCounts = append(be.BatchUpdateCounts, int64(-1))
} else {
break
}
}
}
return nil, be
}
return nil, err
}
mode := propertyDDLExecutionMode.GetValueOrDefault(c.state)
if mode == DDLExecutionModeAsync || mode == DDLExecutionModeAsyncWait {
return &result{operationID: op.Name()}, nil
}
return c.executeDDLWithDefaultSequenceKindRetry(ctx, statements, ddlStatements)
}
return driver.ResultNoRows, nil
}
Expand Down
9 changes: 9 additions & 0 deletions connection_properties.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ var propertyReadOnlyStaleness = createConnectionProperty(
connectionstate.ContextUser,
connectionstate.ConvertReadOnlyStaleness,
)
var propertyDefaultSequenceKind = createConnectionProperty(
"default_sequence_kind",
"The default sequence kind to automatically set if a DDL statement fails due to missing sequence kind.",
"",
false,
nil,
connectionstate.ContextUser,
connectionstate.ConvertString,
)

var propertyAutoPartitionMode = createConnectionProperty(
"auto_partition_mode",
Expand Down
151 changes: 151 additions & 0 deletions default_sequence_kind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package spannerdriver

import (
"context"
"database/sql/driver"
"fmt"
"regexp"
"strings"

"cloud.google.com/go/spanner"
adminapi "cloud.google.com/go/spanner/admin/database/apiv1"
adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
)

var reMissingDefaultSequenceKind = regexp.MustCompile(`Please specify the sequence kind explicitly or set the database option\s+['\x60]?default_sequence_kind['\x60]?\.`)

func isMissingDefaultSequenceKindError(err error) bool {
if err == nil {
return false
}
return reMissingDefaultSequenceKind.MatchString(err.Error())
}

func (c *conn) executeDDLWithDefaultSequenceKindRetry(ctx context.Context, originalStatements []spanner.Statement, ddlStatements []string) (driver.Result, error) {
op, err := c.adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: c.database,
Statements: ddlStatements,
})

var opRetry *adminapi.UpdateDatabaseDdlOperation
var restartIndex int
var retryErr error

if err != nil {
// The RPC execution returned an error.
defaultSequenceKind := propertyDefaultSequenceKind.GetValueOrDefault(c.state)
if defaultSequenceKind != "" && isMissingDefaultSequenceKindError(err) {
if errAlter := c.setDefaultSequenceKind(ctx, defaultSequenceKind); errAlter == nil {
opRetry, retryErr = c.adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: c.database,
Statements: ddlStatements,
})
}
}
} else {
c.lastDDLOperationID = op.Name()
err = c.waitForDDLOperation(ctx, op.Name(), func(ctx context.Context) error {
return op.Wait(ctx)
})
if err != nil {
// The long-running operation returned an error.
defaultSequenceKind := propertyDefaultSequenceKind.GetValueOrDefault(c.state)
if defaultSequenceKind != "" && isMissingDefaultSequenceKindError(err) {
if errAlter := c.setDefaultSequenceKind(ctx, defaultSequenceKind); errAlter == nil {
restartIndex = getSuccessCount(op)
if restartIndex < len(ddlStatements) {
opRetry, retryErr = c.adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: c.database,
Statements: ddlStatements[restartIndex:],
})
}
}
}
}
}

// If a retry was successfully scheduled
if opRetry != nil && retryErr == nil {
c.lastDDLOperationID = opRetry.Name()
err = c.waitForDDLOperation(ctx, opRetry.Name(), func(ctx context.Context) error {
return opRetry.Wait(ctx)
})
if err == nil {
mode := propertyDDLExecutionMode.GetValueOrDefault(c.state)
if mode == DDLExecutionModeAsync || mode == DDLExecutionModeAsyncWait {
return &result{operationID: opRetry.Name()}, nil
}
return driver.ResultNoRows, nil
}
} else if retryErr != nil {
err = retryErr
}

if err != nil {
if len(originalStatements) > 1 {
be := &BatchError{
Err: err,
BatchUpdateCounts: []int64{},
}
successCount := getSuccessCount(op)
if opRetry != nil {
successCount = restartIndex + getSuccessCount(opRetry)
}
for i := 0; i < successCount; i++ {
be.BatchUpdateCounts = append(be.BatchUpdateCounts, int64(-1))
}
return nil, be
}
return nil, err
}

mode := propertyDDLExecutionMode.GetValueOrDefault(c.state)
if mode == DDLExecutionModeAsync || mode == DDLExecutionModeAsyncWait {
return &result{operationID: op.Name()}, nil
}
return driver.ResultNoRows, nil
}

func (c *conn) setDefaultSequenceKind(ctx context.Context, defaultSequenceKind string) error {
dbID := c.databaseID()
var alterStatement string
if c.parser.Dialect == adminpb.DatabaseDialect_POSTGRESQL {
alterStatement = fmt.Sprintf(`ALTER DATABASE "%s" SET spanner.default_sequence_kind = '%s'`, strings.ReplaceAll(dbID, `"`, `""`), defaultSequenceKind)
} else {
alterStatement = fmt.Sprintf("ALTER DATABASE `%s` SET OPTIONS (default_sequence_kind = '%s')", strings.ReplaceAll(dbID, "`", "``"), defaultSequenceKind)
}
opAlter, errAlter := c.adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
Database: c.database,
Statements: []string{alterStatement},
})
if errAlter != nil {
return errAlter
}
return c.waitForDDLOperation(ctx, opAlter.Name(), func(ctx context.Context) error {
return opAlter.Wait(ctx)
})
}

func (c *conn) databaseID() string {
parts := strings.Split(c.database, "/")
return parts[len(parts)-1]
}

func getSuccessCount(op *adminapi.UpdateDatabaseDdlOperation) int {
if op == nil {
return 0
}
metadata, err := op.Metadata()
if err != nil || metadata == nil {
return 0
}
var count int
for _, ts := range metadata.CommitTimestamps {
if ts != nil {
count++
} else {
break
}
}
return count
}
12 changes: 12 additions & 0 deletions driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1087,3 +1087,15 @@ func TestConnectionStateTypeInitialization(t *testing.T) {
t.Errorf("ConnectionStateType mismatch. Got: %v, Want: %v", c.connectorConfig.ConnectionStateType, connectionstate.TypeTransactional)
}
}

func TestIsMissingDefaultSequenceKindError(t *testing.T) {
err := fmt.Errorf("rpc error: code = InvalidArgument desc = The sequence kind of an identity column id is not specified. Please specify the sequence kind explicitly or set the database option `default_sequence_kind`.")
if !isMissingDefaultSequenceKindError(err) {
t.Errorf("isMissingDefaultSequenceKindError returned false for: %v", err)
}

errNoQuotes := fmt.Errorf("rpc error: code = InvalidArgument desc = The sequence kind of an identity column id is not specified. Please specify the sequence kind explicitly or set the database option default_sequence_kind.")
if !isMissingDefaultSequenceKindError(errNoQuotes) {
t.Errorf("isMissingDefaultSequenceKindError returned false for: %v", errNoQuotes)
}
}
Loading
Loading