Skip to content

Commit 565ebde

Browse files
committed
Merge branch 'timestamp-bound-exec-option' into ado-net-driver
2 parents cd2a8f5 + e120e88 commit 565ebde

File tree

9 files changed

+382
-18
lines changed

9 files changed

+382
-18
lines changed

conn.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ type SpannerConn interface {
225225
// DetectStatementType returns the type of SQL statement.
226226
DetectStatementType(query string) parser.StatementType
227227

228+
// Parser returns the parser.StatementParser that is used for this connection.
229+
Parser() *parser.StatementParser
230+
228231
// resetTransactionForRetry resets the current transaction after it has
229232
// been aborted by Spanner. Calling this function on a transaction that
230233
// has not been aborted is not supported and will cause an error to be
@@ -265,6 +268,7 @@ type conn struct {
265268
tx *delegatingTransaction
266269
prevTx *delegatingTransaction
267270
resetForRetry bool
271+
instance string
268272
database string
269273

270274
execSingleQuery func(ctx context.Context, c *spanner.Client, statement spanner.Statement, statementInfo *parser.StatementInfo, bound spanner.TimestampBound, options *ExecOptions) *spanner.RowIterator
@@ -294,6 +298,10 @@ func (c *conn) DetectStatementType(query string) parser.StatementType {
294298
return info.StatementType
295299
}
296300

301+
func (c *conn) Parser() *parser.StatementParser {
302+
return c.parser
303+
}
304+
297305
func (c *conn) CommitTimestamp() (time.Time, error) {
298306
ts := propertyCommitTimestamp.GetValueOrDefault(c.state)
299307
if ts == nil {
@@ -420,6 +428,16 @@ func (c *conn) setReadOnlyStaleness(staleness spanner.TimestampBound) (driver.Re
420428
return driver.ResultNoRows, nil
421429
}
422430

431+
func (c *conn) readOnlyStalenessPointer() *spanner.TimestampBound {
432+
val := propertyReadOnlyStaleness.GetConnectionPropertyValue(c.state)
433+
if val == nil || !val.HasValue() {
434+
return nil
435+
}
436+
staleness, _ := val.GetValue()
437+
timestampBound := staleness.(spanner.TimestampBound)
438+
return &timestampBound
439+
}
440+
423441
func (c *conn) IsolationLevel() sql.IsolationLevel {
424442
return propertyIsolationLevel.GetValueOrDefault(c.state)
425443
}
@@ -650,6 +668,22 @@ func (c *conn) execDDL(ctx context.Context, statements ...spanner.Statement) (dr
650668
for i, s := range statements {
651669
ddlStatements[i] = s.SQL
652670
}
671+
if c.parser.IsCreateDatabaseStatement(ddlStatements[0]) {
672+
op, err := c.adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
673+
CreateStatement: ddlStatements[0],
674+
DatabaseDialect: c.parser.Dialect,
675+
Parent: c.instance,
676+
ExtraStatements: ddlStatements[1:],
677+
})
678+
if err != nil {
679+
return nil, err
680+
}
681+
if _, err := op.Wait(ctx); err != nil {
682+
return nil, err
683+
}
684+
return driver.ResultNoRows, nil
685+
}
686+
653687
op, err := c.adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{
654688
Database: c.database,
655689
Statements: ddlStatements,
@@ -1205,6 +1239,7 @@ func (c *conn) options(reset bool) (*ExecOptions, error) {
12051239
},
12061240
},
12071241
PartitionedQueryOptions: PartitionedQueryOptions{},
1242+
TimestampBound: c.readOnlyStalenessPointer(),
12081243
}
12091244
if c.tempExecOptions != nil {
12101245
effectiveOptions.merge(c.tempExecOptions)
@@ -1585,6 +1620,9 @@ func (c *conn) Rollback(ctx context.Context) error {
15851620
}
15861621

15871622
func queryInSingleUse(ctx context.Context, c *spanner.Client, statement spanner.Statement, statementInfo *parser.StatementInfo, tb spanner.TimestampBound, options *ExecOptions) *spanner.RowIterator {
1623+
if options.TimestampBound != nil {
1624+
tb = *options.TimestampBound
1625+
}
15881626
return c.Single().WithTimestampBound(tb).QueryWithOptions(ctx, statement, options.QueryOptions)
15891627
}
15901628

conn_with_mockserver_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,34 @@ func TestSetLocalReadLockMode(t *testing.T) {
681681
}
682682
}
683683

684+
func TestTimestampBound(t *testing.T) {
685+
t.Parallel()
686+
687+
db, server, teardown := setupTestDBConnection(t)
688+
defer teardown()
689+
ctx := context.Background()
690+
691+
staleness := spanner.MaxStaleness(10 * time.Second)
692+
row := db.QueryRowContext(ctx, testutil.SelectFooFromBar, ExecOptions{TimestampBound: &staleness})
693+
if row.Err() != nil {
694+
t.Fatal(row.Err())
695+
}
696+
var val int64
697+
if err := row.Scan(&val); err != nil {
698+
t.Fatal(err)
699+
}
700+
701+
requests := server.TestSpanner.DrainRequestsFromServer()
702+
executeRequests := testutil.RequestsOfType(requests, reflect.TypeOf(&spannerpb.ExecuteSqlRequest{}))
703+
if g, w := len(executeRequests), 1; g != w {
704+
t.Fatalf("execute requests count mismatch\n Got: %v\nWant: %v", g, w)
705+
}
706+
request := executeRequests[0].(*spannerpb.ExecuteSqlRequest)
707+
if g, w := request.Transaction.GetSingleUse().GetReadOnly().GetMaxStaleness().GetSeconds(), int64(10); g != w {
708+
t.Fatalf("read staleness mismatch\n Got: %v\nWant: %v", g, w)
709+
}
710+
}
711+
684712
func TestCreateDatabase(t *testing.T) {
685713
t.Parallel()
686714

driver.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ type ExecOptions struct {
173173
TransactionOptions spanner.TransactionOptions
174174
// QueryOptions are the query options that will be used for the statement.
175175
QueryOptions spanner.QueryOptions
176+
// TimestampBound is the timestamp bound that will be used for the statement
177+
// if it is a query outside a transaction. Setting this option will override
178+
// the default TimestampBound that is set on the connection.
179+
TimestampBound *spanner.TimestampBound
176180

177181
// PartitionedQueryOptions are used for partitioned queries, and ignored
178182
// for all other statements.
@@ -246,6 +250,9 @@ func (dest *ExecOptions) merge(src *ExecOptions) {
246250
if src.AutocommitDMLMode != Unspecified {
247251
dest.AutocommitDMLMode = src.AutocommitDMLMode
248252
}
253+
if src.TimestampBound != nil {
254+
dest.TimestampBound = src.TimestampBound
255+
}
249256
if src.PropertyValues != nil {
250257
dest.PropertyValues = append(dest.PropertyValues, src.PropertyValues...)
251258
}
@@ -742,6 +749,10 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
742749
func openDriverConn(ctx context.Context, c *connector) (driver.Conn, error) {
743750
opts := c.options
744751
c.logger.Log(ctx, LevelNotice, "opening connection")
752+
instanceName := fmt.Sprintf(
753+
"projects/%s/instances/%s",
754+
c.connectorConfig.Project,
755+
c.connectorConfig.Instance)
745756
databaseName := fmt.Sprintf(
746757
"projects/%s/instances/%s/databases/%s",
747758
c.connectorConfig.Project,
@@ -780,6 +791,7 @@ func openDriverConn(ctx context.Context, c *connector) (driver.Conn, error) {
780791
adminClient: c.adminClient,
781792
connId: connId,
782793
logger: logger,
794+
instance: instanceName,
783795
database: databaseName,
784796
state: createInitialConnectionState(connectionStateType, c.initialPropertyValues),
785797
execSingleQuery: queryInSingleUse,

drivers/spanner-ado-net/spanner-ado-net-specification-tests/GetValueConversionTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,41 @@ public class GetValueConversionTests(DbFactoryFixture fixture) : GetValueConvers
135135

136136
public override async Task GetInt64_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync<long>(0), 1L);
137137

138+
// GetByte throws OverflowException instead of InvalidCastException if the value is out of range.
139+
public override void GetByte_throws_for_maximum_Decimal() => TestException(DbType.Decimal, ValueKind.Maximum, x => x.GetByte(0), typeof(OverflowException));
140+
141+
public override void GetByte_throws_for_maximum_Double() => TestException(DbType.Double, ValueKind.Maximum, x => x.GetByte(0), typeof(OverflowException));
142+
143+
public override void GetByte_throws_for_maximum_Int64() => TestException(DbType.Int64, ValueKind.Maximum, x => x.GetByte(0), typeof(OverflowException));
144+
145+
public override void GetByte_throws_for_maximum_Single() => TestException(DbType.Single, ValueKind.Maximum, x => x.GetByte(0), typeof(OverflowException));
146+
147+
public override void GetByte_throws_for_minimum_Decimal() => TestException(DbType.Decimal, ValueKind.Minimum, x => x.GetByte(0), typeof(OverflowException));
148+
149+
public override void GetByte_throws_for_minimum_Double() => TestGetValue(DbType.Double, ValueKind.Minimum, x => x.GetByte(0), (byte)0);
150+
151+
public override void GetByte_throws_for_minimum_Int64() => TestException(DbType.Int64, ValueKind.Minimum, x => x.GetByte(0), typeof(OverflowException));
152+
153+
public override void GetByte_throws_for_minimum_Single() => TestGetValue(DbType.Single, ValueKind.Minimum, x => x.GetByte(0), (byte)0);
154+
155+
public override void GetByte_throws_for_one_Decimal() => TestGetValue(DbType.Decimal, ValueKind.One, x => x.GetByte(0), (byte)1);
156+
157+
public override void GetByte_throws_for_one_Double() => TestGetValue(DbType.Double, ValueKind.One, x => x.GetByte(0), (byte)1);
158+
159+
public override void GetByte_throws_for_one_Int64() => TestGetValue(DbType.Int64, ValueKind.One, x => x.GetByte(0), (byte)1);
160+
161+
public override void GetByte_throws_for_one_Single() => TestGetValue(DbType.Single, ValueKind.One, x => x.GetByte(0), (byte)1);
162+
163+
public override void GetByte_throws_for_one_String() => TestGetValue(DbType.String, ValueKind.One, x => x.GetByte(0), (byte)1);
164+
165+
public override void GetByte_throws_for_zero_Decimal() => TestGetValue(DbType.Decimal, ValueKind.Zero, x => x.GetByte(0), (byte)0);
166+
167+
public override void GetByte_throws_for_zero_Double() => TestGetValue(DbType.Double, ValueKind.Zero, x => x.GetByte(0), (byte)0);
168+
169+
public override void GetByte_throws_for_zero_Int64() => TestGetValue(DbType.Int64, ValueKind.Zero, x => x.GetByte(0), (byte)0);
170+
171+
public override void GetByte_throws_for_zero_Single() => TestGetValue(DbType.Single, ValueKind.Zero, x => x.GetByte(0), (byte)0);
172+
173+
public override void GetByte_throws_for_zero_String() => TestGetValue(DbType.String, ValueKind.Zero, x => x.GetByte(0), (byte)0);
174+
138175
}

drivers/spanner-ado-net/spanner-ado-net-tests/BasicTests.cs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414

1515
using System.Data.Common;
1616
using System.Text.Json;
17+
using Google.Cloud.Spanner.Admin.Database.V1;
1718
using Google.Cloud.Spanner.V1;
1819
using Google.Cloud.SpannerLib.MockServer;
20+
using Google.Protobuf.WellKnownTypes;
1921

2022
namespace Google.Cloud.Spanner.DataProvider.Tests;
2123

@@ -66,6 +68,34 @@ public void TestExecuteParameterizedQuery()
6668
Assert.That(reader.GetInt64(0), Is.EqualTo(1));
6769
}
6870
}
71+
72+
[Test]
73+
public void TestExecuteStaleQuery()
74+
{
75+
using var connection = new SpannerConnection();
76+
connection.ConnectionString = ConnectionString;
77+
connection.Open();
78+
79+
using var cmd = connection.CreateCommand();
80+
cmd.CommandText = "SELECT 1";
81+
cmd.SingleUseReadOnlyTransactionOptions = new TransactionOptions.Types.ReadOnly
82+
{
83+
ExactStaleness = Duration.FromTimeSpan(TimeSpan.FromSeconds(10)),
84+
};
85+
using var reader = cmd.ExecuteReader();
86+
while (reader.Read())
87+
{
88+
Assert.That(reader.GetInt64(0), Is.EqualTo(1));
89+
}
90+
91+
var request = Fixture.SpannerMock.Requests.OfType<ExecuteSqlRequest>().First();
92+
Assert.That(request, Is.Not.Null);
93+
Assert.That(request.Transaction, Is.Not.Null);
94+
Assert.That(request.Transaction.SingleUse, Is.Not.Null);
95+
Assert.That(request.Transaction.SingleUse.ReadOnly, Is.Not.Null);
96+
Assert.That(request.Transaction.SingleUse.ReadOnly.ExactStaleness, Is.Not.Null);
97+
Assert.That(request.Transaction.SingleUse.ReadOnly.ExactStaleness.Seconds, Is.EqualTo(10));
98+
}
6999

70100
[Test]
71101
public void TestInsertAllDataTypes()
@@ -112,7 +142,78 @@ public void TestInsertAllDataTypes()
112142

113143
Assert.That(request.ParamTypes.Count, Is.EqualTo(0));
114144
}
145+
146+
[Test]
147+
public void TestExecuteDdl()
148+
{
149+
using var connection = new SpannerConnection();
150+
connection.ConnectionString = ConnectionString;
151+
connection.Open();
152+
153+
using var cmd = connection.CreateCommand();
154+
cmd.CommandText = "create table my_table (id int64 primary key, value string(max))";
155+
Assert.That(cmd.ExecuteNonQuery(), Is.EqualTo(-1));
115156

157+
var requests = Fixture.DatabaseAdminMock.Requests.OfType<UpdateDatabaseDdlRequest>().ToList();
158+
Assert.That(requests, Has.Count.EqualTo(1));
159+
var request = requests.First();
160+
Assert.That(request.Statements, Has.Count.EqualTo(1));
161+
var statement = request.Statements.First();
162+
Assert.That(statement, Is.EqualTo(cmd.CommandText));
163+
}
164+
165+
[Test]
166+
public void TestExecuteCreateDatabase()
167+
{
168+
using var connection = new SpannerConnection();
169+
connection.ConnectionString = ConnectionString;
170+
connection.Open();
171+
172+
using var cmd = connection.CreateCommand();
173+
cmd.CommandText = "create database my_database";
174+
Assert.That(cmd.ExecuteNonQuery(), Is.EqualTo(-1));
175+
176+
var updateDatabaseDdlRequests = Fixture.DatabaseAdminMock.Requests.OfType<UpdateDatabaseDdlRequest>().ToList();
177+
Assert.That(updateDatabaseDdlRequests, Has.Count.EqualTo(0));
178+
179+
var requests = Fixture.DatabaseAdminMock.Requests.OfType<CreateDatabaseRequest>().ToList();
180+
Assert.That(requests, Has.Count.EqualTo(1));
181+
var request = requests.First();
182+
Assert.That(request.CreateStatement, Is.EqualTo(cmd.CommandText));
183+
Assert.That(request.Parent, Is.EqualTo("projects/p1/instances/i1"));
184+
}
185+
186+
[Test]
187+
public void TestExecuteCreateDatabaseWithExtraStatements()
188+
{
189+
using var connection = new SpannerConnection();
190+
connection.ConnectionString = ConnectionString;
191+
connection.Open();
192+
193+
using var batch = connection.CreateBatch();
194+
var cmd = batch.CreateBatchCommand();
195+
cmd.CommandText = "create database my_database";
196+
batch.BatchCommands.Add(cmd);
197+
cmd = batch.CreateBatchCommand();
198+
cmd.CommandText = "create table my_table (id int64 primary key, value string)";
199+
batch.BatchCommands.Add(cmd);
200+
cmd = batch.CreateBatchCommand();
201+
cmd.CommandText = "create index my_index on my_table (value)";
202+
batch.BatchCommands.Add(cmd);
203+
204+
// TODO: Check with other drivers what they return when a batch contains multiple statements that return -1.
205+
Assert.That(batch.ExecuteNonQuery(), Is.EqualTo(-3));
206+
207+
var updateDatabaseDdlRequests = Fixture.DatabaseAdminMock.Requests.OfType<UpdateDatabaseDdlRequest>().ToList();
208+
Assert.That(updateDatabaseDdlRequests, Has.Count.EqualTo(0));
209+
210+
var requests = Fixture.DatabaseAdminMock.Requests.OfType<CreateDatabaseRequest>().ToList();
211+
Assert.That(requests, Has.Count.EqualTo(1));
212+
var request = requests.First();
213+
Assert.That(request.CreateStatement, Is.EqualTo("create database my_database"));
214+
Assert.That(request.Parent, Is.EqualTo("projects/p1/instances/i1"));
215+
}
216+
116217
private void AddParameter(DbCommand command, string name, object value)
117218
{
118219
var param = command.CreateParameter();

drivers/spanner-ado-net/spanner-ado-net/SpannerDataReader.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,29 @@ public override bool GetBoolean(int ordinal)
306306

307307
public override byte GetByte(int ordinal)
308308
{
309-
CheckValidPosition();
309+
var value = GetProtoValue(ordinal);
310310
CheckNotNull(ordinal);
311+
if (value.HasStringValue)
312+
{
313+
try
314+
{
315+
return byte.Parse(value.StringValue,
316+
NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign | NumberStyles.AllowExponent,
317+
CultureInfo.InvariantCulture);
318+
}
319+
catch (OverflowException)
320+
{
321+
throw;
322+
}
323+
catch (Exception exception)
324+
{
325+
throw new InvalidCastException(exception.Message, exception);
326+
}
327+
}
328+
if (value.HasNumberValue)
329+
{
330+
return checked((byte)value.NumberValue);
331+
}
311332
throw new InvalidCastException("not a valid byte value");
312333
}
313334

@@ -596,7 +617,7 @@ public override short GetInt16(int ordinal)
596617
}
597618
if (value.HasNumberValue)
598619
{
599-
return (short)value.NumberValue;
620+
return checked((short)value.NumberValue);
600621
}
601622
throw new InvalidCastException("not a valid Int16 value");
602623
}
@@ -624,7 +645,7 @@ public override int GetInt32(int ordinal)
624645
}
625646
if (value.HasNumberValue)
626647
{
627-
return (int)value.NumberValue;
648+
return checked((int)value.NumberValue);
628649
}
629650
throw new InvalidCastException("not a valid Int32 value");
630651
}
@@ -648,7 +669,7 @@ public override long GetInt64(int ordinal)
648669
}
649670
if (value.HasNumberValue)
650671
{
651-
return (long)value.NumberValue;
672+
return checked((long)value.NumberValue);
652673
}
653674
throw new InvalidCastException("not a valid Int64 value");
654675
}

parser/statement_parser.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,3 +805,11 @@ func detectDmlKeyword(keyword string) DmlType {
805805
}
806806
return DmlTypeUnknown
807807
}
808+
809+
func (p *StatementParser) IsCreateDatabaseStatement(sql string) bool {
810+
return isCreateDatabase(p, sql)
811+
}
812+
813+
func (p *StatementParser) IsDropDatabaseStatement(sql string) bool {
814+
return isDropDatabase(p, sql)
815+
}

0 commit comments

Comments
 (0)