Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
d1d3109
chore: create shared lib for connections
olavloite May 16, 2025
3447ae4
Merge branch 'main' into spanner-lib
olavloite May 29, 2025
4d7aad9
chore: add more features
olavloite Jun 4, 2025
21a1c4a
Merge branch 'main' into spanner-lib
olavloite Jun 10, 2025
93f38ba
feat: add transactions
olavloite Jun 12, 2025
312ff67
docs: add code comments
olavloite Jun 16, 2025
f898b49
Merge branch 'main' into spanner-lib
olavloite Jun 16, 2025
251d4ce
feat: move metadata and stats to separate result sets
olavloite Jun 16, 2025
8ac5e6b
feat: add option to return metadata and stats
olavloite Jun 17, 2025
304082f
Merge branch 'main' into return-metadata-and-stats
olavloite Jun 17, 2025
f9ff983
Merge branch 'return-metadata-and-stats' into spanner-lib
olavloite Jun 18, 2025
2de8b88
feat: support timestampbound for single-use read-only tx
olavloite Jun 19, 2025
6667833
test: add more tests
olavloite Jun 23, 2025
3a1ce86
feat: add mutations support
olavloite Jun 25, 2025
91cf56c
test: add tests for dotnet
olavloite Jun 25, 2025
b819fd9
Merge branch 'main' into spanner-lib
olavloite Jun 25, 2025
3db43be
Merge branch 'main' into spanner-lib
olavloite Jun 26, 2025
872a83d
chore: go mod tidy
olavloite Jun 26, 2025
ce6db93
chore: refactor dotnet lib
olavloite Jun 26, 2025
9ab50f2
chore: use separate classes per arch
olavloite Jun 26, 2025
4b910fb
chore: update description
olavloite Jun 27, 2025
2cf0e32
chore: change libraries to packed content
olavloite Jun 27, 2025
cc9fc07
chore: refactor dotnet lib
olavloite Jun 30, 2025
c25dcee
chore: separate native lib + use standard dotnet runtime setup
olavloite Jun 30, 2025
56d667e
Merge branch 'main' into spanner-lib
olavloite Jun 30, 2025
e83455c
chore: cleanup
olavloite Jun 30, 2025
9a4249b
Merge branch 'main' into spanner-lib
olavloite Jul 2, 2025
9a33505
chore: add benchmark
olavloite Jul 3, 2025
6a6999e
Merge branch 'main' into spanner-lib
olavloite Jul 3, 2025
dc717a3
chore: bump native lib version
olavloite Jul 3, 2025
94a3746
chore: bump version for both dotnet libs
olavloite Jul 3, 2025
bfc8923
refactor: add grpc server
olavloite Jul 8, 2025
aaba345
chore: test with CPP lib
olavloite Aug 5, 2025
5fccbde
Merge branch 'main' into spanner-lib
olavloite Aug 20, 2025
4825aa4
chore: add generic transactional connection state
olavloite Aug 21, 2025
1803771
chore: move connection variables to connection state
olavloite Aug 22, 2025
7f78b3b
chore: parse SET/SHOW statements with simple parser
olavloite Aug 25, 2025
5da0cbe
chore: use a callback to supply tx opts
olavloite Aug 26, 2025
cf68db7
chore: replace ExecOptions for connection variables
olavloite Aug 27, 2025
d7b55fc
Merge branch 'main' into replace-exec-options-with-connection-variables
olavloite Aug 28, 2025
72aa0c8
Merge branch 'main' into replace-exec-options-with-connection-variables
olavloite Aug 28, 2025
abc6e5b
Merge branch 'replace-exec-options-with-connection-variables' into sp…
olavloite Aug 28, 2025
88f2dbf
feat: create/drop database statements
olavloite Aug 30, 2025
bd4e2b4
Merge branch 'create-drop-database' into spanner-lib
olavloite Aug 30, 2025
d0a5fc3
feat: create/drop database statements
olavloite Aug 30, 2025
5d9f46e
Merge branch 'create-drop-database' into spanner-lib
olavloite Aug 30, 2025
2c18d41
Merge branch 'main' into spanner-lib
olavloite Sep 4, 2025
5a193a8
chore: cache parsed statements and remove regex loading
olavloite Sep 5, 2025
617308d
chore: move parser to separate package
olavloite Sep 5, 2025
c299895
Merge branch 'main' into move-parser-to-separate-package
olavloite Sep 6, 2025
8064e3d
chore: cleanup and add comments
olavloite Sep 6, 2025
16dd72e
Merge branch 'move-parser-to-separate-package' into spanner-lib
olavloite Sep 7, 2025
9c99bcb
chore: add more tests
olavloite Sep 8, 2025
448daab
Merge branch 'main' into spanner-lib
olavloite Sep 8, 2025
d3fe254
Merge branch 'main' into spanner-lib
olavloite Sep 8, 2025
dd4ed73
chore: remove the backend package
olavloite Sep 8, 2025
0e5ac05
chore: pull retry_aborts_internally when needed
olavloite Sep 8, 2025
928ee4d
chore: add options for numRows and encoding
olavloite Sep 8, 2025
23f6ea6
Merge branch 'pull-retry-aborts' into spanner-lib
olavloite Sep 8, 2025
ec75601
feat: support BEGIN, COMMIT and ROLLBACK statements
olavloite Sep 9, 2025
2bf6e3e
Merge branch 'tx-statements' into spanner-lib
olavloite Sep 9, 2025
900e003
chore: remove transaction id from api
olavloite Sep 10, 2025
c7c0792
Merge branch 'main' into spanner-lib
olavloite Sep 10, 2025
013a61c
chore: add .NET specification tests
olavloite Sep 11, 2025
f6366d4
chore: remove as many proto files as possible
olavloite Sep 12, 2025
5b272c9
Merge branch 'main' into spanner-lib
olavloite Sep 12, 2025
bd774ef
chore: remove more protos
olavloite Sep 12, 2025
63f4ac6
chore: remove unused code
olavloite Sep 12, 2025
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
89 changes: 89 additions & 0 deletions .github/workflows/spanner-lib-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
on:
push:
branches: [ main ]
pull_request:
permissions:
contents: read
pull-requests: write
name: Spanner Lib Tests
jobs:
test:
strategy:
matrix:
go-version: [1.24.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- name: Run unit tests
working-directory: spannerlib/lib
run: go test -race -short

build-lib:
strategy:
matrix:
go-version: [1.24.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- name: Build shared lib
working-directory: spannerlib/shared
run: go build -o spannerlib.so -buildmode=c-shared shared_lib.go

test-dotnet-ubuntu:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: 1.24
- name: Checkout code
uses: actions/checkout@v4
- name: Build shared lib
working-directory: spannerlib/shared
run: go build -o spannerlib.so -buildmode=c-shared shared_lib.go
- name: Copy lib to dotnet folder
working-directory: spannerlib
run: |
mkdir -p dotnet-spannerlib/Google.Cloud.SpannerLib.Native/libraries/any
cp shared/spannerlib.so dotnet-spannerlib/Google.Cloud.SpannerLib.Native/libraries/any/spannerlib.so
- name: Build gRPC server
working-directory: spannerlib/grpc-server
run: go build grpc_server.go
- name: Copy gRPC server to dotnet folder
working-directory: spannerlib
run: |
mkdir -p dotnet-spannerlib/Google.Cloud.SpannerLib.Grpc/binaries/any
cp grpc-server/grpc_server dotnet-spannerlib/Google.Cloud.SpannerLib.Grpc/binaries/any/grpc_server
- name: Install dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: dotnet version
run: dotnet --version
- name: Build native library package
run: dotnet pack
working-directory: spannerlib/dotnet-spannerlib/Google.Cloud.SpannerLib.Native
- name: Add package source
run: dotnet nuget add source "$PWD"/bin/Release --name local
working-directory: spannerlib/dotnet-spannerlib/Google.Cloud.SpannerLib.Native
- name: Restore dependencies
run: dotnet restore
working-directory: spannerlib/dotnet-spannerlib
- name: Build
run: dotnet build --no-restore
working-directory: spannerlib/dotnet-spannerlib
- name: Unit Tests
working-directory: spannerlib/dotnet-spannerlib/Google.Cloud.SpannerLib.Tests
run: dotnet test --no-build --verbosity normal
17 changes: 7 additions & 10 deletions checksum_row_iterator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"cloud.google.com/go/spanner"
sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
"github.com/googleapis/go-sql-spanner/parser"
"google.golang.org/api/iterator"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -51,10 +52,11 @@ type checksumRowIterator struct {
*spanner.RowIterator
metadata *sppb.ResultSetMetadata

ctx context.Context
tx *readWriteTransaction
stmt spanner.Statement
options spanner.QueryOptions
ctx context.Context
tx *readWriteTransaction
stmt spanner.Statement
stmtType parser.StatementType
options spanner.QueryOptions
// nc (nextCount) indicates the number of times that next has been called
// on the iterator. Next() will be called the same number of times during
// a retry.
Expand Down Expand Up @@ -253,10 +255,5 @@ func (it *checksumRowIterator) Metadata() (*sppb.ResultSetMetadata, error) {
}

func (it *checksumRowIterator) ResultSetStats() *sppb.ResultSetStats {
// TODO: The Spanner client library should offer an option to get the full
// ResultSetStats, instead of only the RowCount and QueryPlan.
return &sppb.ResultSetStats{
RowCount: &sppb.ResultSetStats_RowCountExact{RowCountExact: it.RowIterator.RowCount},
QueryPlan: it.RowIterator.QueryPlan,
}
return createResultSetStats(it.RowIterator, it.stmtType)
}
67 changes: 62 additions & 5 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ type SpannerConn interface {
// return the same Spanner client.
UnderlyingClient() (client *spanner.Client, err error)

// DetectStatementType returns the type of SQL statement.
DetectStatementType(query string) parser.StatementType

// resetTransactionForRetry resets the current transaction after it has
// been aborted by Spanner. Calling this function on a transaction that
// has not been aborted is not supported and will cause an error to be
Expand Down Expand Up @@ -280,12 +283,18 @@ type conn struct {
// tempBatchReadOnlyTransactionOptions are temporarily set right before a
// batch read-only transaction is started on a Spanner connection.
tempBatchReadOnlyTransactionOptions *BatchReadOnlyTransactionOptions
tempProtoTransactionOptions *spannerpb.TransactionOptions
}

func (c *conn) UnderlyingClient() (*spanner.Client, error) {
return c.client, nil
}

func (c *conn) DetectStatementType(query string) parser.StatementType {
info := c.parser.DetectStatementType(query)
return info.StatementType
}

func (c *conn) CommitTimestamp() (time.Time, error) {
ts := propertyCommitTimestamp.GetValueOrDefault(c.state)
if ts == nil {
Expand Down Expand Up @@ -389,6 +398,16 @@ func (c *conn) ReadOnlyStaleness() spanner.TimestampBound {
return propertyReadOnlyStaleness.GetValueOrDefault(c.state)
}

func (c *conn) readOnlyStalenessPointer() *spanner.TimestampBound {
val := propertyReadOnlyStaleness.GetConnectionPropertyValue(c.state)
if val == nil || !val.HasValue() {
return nil
}
staleness, _ := val.GetValue()
timestampBound := staleness.(spanner.TimestampBound)
return &timestampBound
}

func (c *conn) SetReadOnlyStaleness(staleness spanner.TimestampBound) error {
_, err := c.setReadOnlyStaleness(staleness)
return err
Expand Down Expand Up @@ -675,6 +694,17 @@ func sum(affected []int64) int64 {
return sum
}

func (c *conn) WriteMutations(ctx context.Context, ms []*spanner.Mutation) (*spanner.CommitResponse, error) {
if c.inTransaction() {
return nil, c.BufferWrite(ms)
}
ts, err := c.Apply(ctx, ms)
if err != nil {
return nil, err
}
return &spanner.CommitResponse{CommitTs: ts}, nil
}

func (c *conn) Apply(ctx context.Context, ms []*spanner.Mutation, opts ...spanner.ApplyOption) (commitTimestamp time.Time, err error) {
if c.inTransaction() {
return time.Time{}, spanner.ToSpannerError(
Expand Down Expand Up @@ -858,13 +888,13 @@ func (c *conn) queryContext(ctx context.Context, query string, execOptions *Exec
// The statement was either detected as being a query, or potentially not recognized at all.
// In that case, just default to using a single-use read-only transaction and let Spanner
// return an error if the statement is not suited for that type of transaction.
iter = &readOnlyRowIterator{c.execSingleQuery(ctx, c.client, stmt, c.ReadOnlyStaleness(), execOptions)}
iter = &readOnlyRowIterator{c.execSingleQuery(ctx, c.client, stmt, c.ReadOnlyStaleness(), execOptions), statementType.StatementType}
}
} else {
if execOptions.PartitionedQueryOptions.PartitionQuery {
return c.tx.partitionQuery(ctx, stmt, execOptions)
}
iter, err = c.tx.Query(ctx, stmt, execOptions)
iter, err = c.tx.Query(ctx, stmt, statementType.StatementType, execOptions)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -987,6 +1017,7 @@ func (c *conn) options(reset bool) *ExecOptions {
},
},
PartitionedQueryOptions: PartitionedQueryOptions{},
TimestampBound: c.readOnlyStalenessPointer(),
}
if c.tempExecOptions != nil {
effectiveOptions.merge(c.tempExecOptions)
Expand Down Expand Up @@ -1071,6 +1102,26 @@ func (c *conn) getBatchReadOnlyTransactionOptions() BatchReadOnlyTransactionOpti
return BatchReadOnlyTransactionOptions{TimestampBound: c.ReadOnlyStaleness()}
}

func (c *conn) BeginReadOnlyTransaction(ctx context.Context, options *ReadOnlyTransactionOptions) (driver.Tx, error) {
c.withTempReadOnlyTransactionOptions(options)
tx, err := c.BeginTx(ctx, driver.TxOptions{ReadOnly: true})
if err != nil {
c.withTempReadOnlyTransactionOptions(nil)
return nil, err
}
return tx, nil
}

func (c *conn) BeginReadWriteTransaction(ctx context.Context, options *ReadWriteTransactionOptions) (driver.Tx, error) {
c.withTempTransactionOptions(options)
tx, err := c.BeginTx(ctx, driver.TxOptions{})
if err != nil {
c.withTempTransactionOptions(nil)
return nil, err
}
return tx, nil
}

func (c *conn) Begin() (driver.Tx, error) {
return c.BeginTx(context.Background(), driver.TxOptions{})
}
Expand Down Expand Up @@ -1254,18 +1305,21 @@ func (c *conn) inReadWriteTransaction() bool {
return false
}

func (c *conn) commit(ctx context.Context) (*spanner.CommitResponse, error) {
func (c *conn) Commit(ctx context.Context) (*spanner.CommitResponse, error) {
if !c.inTransaction() {
return nil, status.Errorf(codes.FailedPrecondition, "this connection does not have a transaction")
}
// TODO: Pass in context to the tx.Commit() function.
if err := c.tx.Commit(); err != nil {
return nil, err
}
return c.CommitResponse()

// This will return either the commit response or nil, depending on whether the transaction was a
// read/write transaction or a read-only transaction.
return propertyCommitResponse.GetValueOrDefault(c.state), nil
}

func (c *conn) rollback(ctx context.Context) error {
func (c *conn) Rollback(ctx context.Context) error {
if !c.inTransaction() {
return status.Errorf(codes.FailedPrecondition, "this connection does not have a transaction")
}
Expand All @@ -1274,6 +1328,9 @@ func (c *conn) rollback(ctx context.Context) error {
}

func queryInSingleUse(ctx context.Context, c *spanner.Client, statement spanner.Statement, tb spanner.TimestampBound, options *ExecOptions) *spanner.RowIterator {
if options.TimestampBound != nil {
tb = *options.TimestampBound
}
return c.Single().WithTimestampBound(tb).QueryWithOptions(ctx, statement, options.QueryOptions)
}

Expand Down
15 changes: 15 additions & 0 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ type ExecOptions struct {
TransactionOptions spanner.TransactionOptions
// QueryOptions are the query options that will be used for the statement.
QueryOptions spanner.QueryOptions
// TimestampBound is the timestamp bound that will be used for the statement
// if it is a query outside a transaction. Setting this option will override
// the default TimestampBound that is set on the connection.
TimestampBound *spanner.TimestampBound

// PartitionedQueryOptions are used for partitioned queries, and ignored
// for all other statements.
Expand Down Expand Up @@ -234,6 +238,9 @@ func (dest *ExecOptions) merge(src *ExecOptions) {
if src.AutocommitDMLMode != Unspecified {
dest.AutocommitDMLMode = src.AutocommitDMLMode
}
if src.TimestampBound != nil {
dest.TimestampBound = src.TimestampBound
}
(&dest.PartitionedQueryOptions).merge(&src.PartitionedQueryOptions)
mergeQueryOptions(&dest.QueryOptions, &src.QueryOptions)
mergeTransactionOptions(&dest.TransactionOptions, &src.TransactionOptions)
Expand Down Expand Up @@ -1143,6 +1150,10 @@ func BeginReadWriteTransaction(ctx context.Context, db *sql.DB, options ReadWrit
// be active when we hit this point.
go conn.Close()
}
return BeginReadWriteTransactionOnConn(ctx, conn, options)
}

func BeginReadWriteTransactionOnConn(ctx context.Context, conn *sql.Conn, options ReadWriteTransactionOptions) (*sql.Tx, error) {
if err := withTempReadWriteTransactionOptions(conn, &options); err != nil {
return nil, err
}
Expand Down Expand Up @@ -1195,6 +1206,10 @@ func BeginReadOnlyTransaction(ctx context.Context, db *sql.DB, options ReadOnlyT
// be active when we hit this point.
go conn.Close()
}
return BeginReadOnlyTransactionOnConn(ctx, conn, options)
}

func BeginReadOnlyTransactionOnConn(ctx context.Context, conn *sql.Conn, options ReadOnlyTransactionOptions) (*sql.Tx, error) {
if err := withTempReadOnlyTransactionOptions(conn, &options); err != nil {
return nil, err
}
Expand Down
4 changes: 3 additions & 1 deletion driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,9 @@ func TestConn_NonDdlStatementsInDdlBatch(t *testing.T) {
state: createInitialConnectionState(connectionstate.TypeNonTransactional, map[string]connectionstate.ConnectionPropertyValue{}),
batch: &batch{tp: parser.BatchTypeDdl},
execSingleQuery: func(ctx context.Context, c *spanner.Client, statement spanner.Statement, tb spanner.TimestampBound, options *ExecOptions) *spanner.RowIterator {
return &spanner.RowIterator{}
return &spanner.RowIterator{
Metadata: &spannerpb.ResultSetMetadata{},
}
},
execSingleDMLTransactional: func(ctx context.Context, c *spanner.Client, statement spanner.Statement, statementInfo *parser.StatementInfo, options *ExecOptions) (*result, *spanner.CommitResponse, error) {
return &result{}, &spanner.CommitResponse{}, nil
Expand Down
5 changes: 2 additions & 3 deletions driver_with_mockserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5531,9 +5531,8 @@ func setupTestDBConnectionWithParams(t *testing.T, params string) (db *sql.DB, s

func setupTestDBConnectionWithParamsAndDialect(t *testing.T, params string, dialect databasepb.DatabaseDialect) (db *sql.DB, server *testutil.MockedSpannerInMemTestServer, teardown func()) {
server, _, serverTeardown := setupMockedTestServerWithDialect(t, dialect)
db, err := sql.Open(
"spanner",
fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true;%s", server.Address, params))
dsn := fmt.Sprintf("%s/projects/p/instances/i/databases/d?useplaintext=true;%s", server.Address, params)
db, err := sql.Open("spanner", dsn)
if err != nil {
serverTeardown()
t.Fatal(err)
Expand Down
3 changes: 2 additions & 1 deletion partitioned_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io"

"cloud.google.com/go/spanner"
"github.com/googleapis/go-sql-spanner/parser"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
Expand Down Expand Up @@ -231,7 +232,7 @@ func (pq *PartitionedQuery) execute(ctx context.Context, index int) (*rows, erro
return nil, spanner.ToSpannerError(status.Errorf(codes.InvalidArgument, "invalid partition index: %d", index))
}
spannerIter := pq.tx.Execute(ctx, pq.Partitions[index])
iter := &readOnlyRowIterator{spannerIter}
iter := &readOnlyRowIterator{spannerIter, parser.StatementTypeQuery}
return &rows{it: iter, decodeOption: pq.execOptions.DecodeOption}, nil
}

Expand Down
3 changes: 3 additions & 0 deletions spannerlib/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
spannerlib.h
spannerlib.so
grpc_server
6 changes: 6 additions & 0 deletions spannerlib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Shared Library (Internal)

__This module can receive breaking changes without prior notice.__

This is an internal module that is used to expose the features in the database/sql driver
to drivers in other programming languages.
6 changes: 6 additions & 0 deletions spannerlib/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Shared Library (Internal)

__This module can receive breaking changes without prior notice.__

This is the common internal API for the various external APIs for exposing the
features in the database/sql driver to drivers in other programming languages.
Loading
Loading