Skip to content

Commit 2ce49aa

Browse files
committed
test: add query all data types test
1 parent 58121f3 commit 2ce49aa

File tree

8 files changed

+211
-21
lines changed

8 files changed

+211
-21
lines changed
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System.Data;
16+
17+
namespace Google.Cloud.Spanner.DataProvider.Samples.Snippets;
18+
19+
/// <summary>
20+
/// This sample shows how to execute a read/write transaction using the Spanner ADO.NET data provider.
21+
/// </summary>
22+
public class TransactionSample
23+
{
24+
public static async Task Run(string connectionString)
25+
{
26+
await using var connection = new SpannerConnection(connectionString);
27+
await connection.OpenAsync();
28+
29+
// Start a read/write transaction by calling th standard BeginTransaction method.
30+
await using var transaction = await connection.BeginTransactionAsync(IsolationLevel.RepeatableRead);
31+
32+
// Execute a query that uses this transaction.
33+
await using var command = connection.CreateCommand();
34+
command.Transaction = transaction;
35+
command.CommandText = "SELECT SingerId " +
36+
"FROM Singers " +
37+
"WHERE BirthDate IS NULL";
38+
var updateCount = 0;
39+
await using var reader = await command.ExecuteReaderAsync();
40+
while (await reader.ReadAsync())
41+
{
42+
// Update the birthdate of each Singer without a known birthdate to a default value.
43+
await using var updateCommand = connection.CreateCommand();
44+
updateCommand.Transaction = transaction;
45+
updateCommand.CommandText = "UPDATE Singers SET BirthDate=DATE '1900-01-01' WHERE SingerId=@singerId";
46+
updateCommand.Parameters.AddWithValue("singerId", reader["SingerId"]);
47+
await updateCommand.ExecuteNonQueryAsync();
48+
updateCount++;
49+
}
50+
await transaction.CommitAsync();
51+
Console.WriteLine($"Set a default birthdate for {updateCount} singers");
52+
}
53+
}

drivers/spanner-ado-net/spanner-ado-net-samples/insert_sample_data.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@
1616
INSERT OR UPDATE INTO Singers (SingerId, FirstName, LastName, BirthDate, Picture) VALUES
1717
(1, 'Mark', 'Richards', DATE '1990-11-09', NULL),
1818
(2, 'Catalina', 'Smith', DATE '1998-04-29', NULL),
19-
(3, 'Alice', 'Trentor', DATE '1979-10-15', NULL);
19+
(3, 'Alice', 'Trentor', DATE '1979-10-15', NULL),
20+
(4, 'Lea', 'Martin', NULL, NULL);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Data;
1516
using AdoNet.Specification.Tests;
1617
using Google.Cloud.SpannerLib.MockServer;
1718
using Xunit;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ public class GetValueConversionTests(DbFactoryFixture fixture) : GetValueConvers
115115

116116
public override void GetString_throws_for_zero_Single() => TestGetValue(DbType.Single, ValueKind.Zero, x => x.GetString(0), "0");
117117

118+
public override void GetString_throws_for_null_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.Null, x => x.GetFieldValue<string>(0), null);
119+
120+
public override async Task GetString_throws_for_null_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.Null, async x => await x.GetFieldValueAsync<string>(0), null);
121+
118122
public override void GetDouble_throws_for_one_String_with_GetFieldValue() => TestGetValue(DbType.String, ValueKind.One, x => x.GetFieldValue<double>(0), 1.0d);
119123

120124
public override async Task GetDouble_throws_for_one_String_with_GetFieldValueAsync() => await TestGetValueAsync(DbType.String, ValueKind.One, async x => await x.GetFieldValueAsync<double>(0), 1.0d);

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,61 @@ public void TestInsertAllDataTypes()
142142

143143
Assert.That(request.ParamTypes.Count, Is.EqualTo(0));
144144
}
145+
146+
[Test]
147+
public void TestQueryAllDataTypes()
148+
{
149+
const string sql = "select * from all_types";
150+
var result = RandomResultSetGenerator.Generate(10, allowNull: true);
151+
Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateQuery(result));
152+
153+
using var connection = new SpannerConnection();
154+
connection.ConnectionString = ConnectionString;
155+
connection.Open();
156+
157+
using var cmd = connection.CreateCommand();
158+
cmd.CommandText = sql;
159+
using var reader = cmd.ExecuteReader();
160+
while (reader.Read())
161+
{
162+
var index = 0;
163+
foreach (var field in result.Metadata.RowType.Fields)
164+
{
165+
Assert.That(reader[index], Is.EqualTo(reader[field.Name]));
166+
index++;
167+
}
168+
Assert.That(reader.GetFieldValue<bool?>(reader.GetOrdinal("col_bool")), Is.EqualTo(ValueOrNull(reader["col_bool"])));
169+
Assert.That(reader.GetFieldValue<byte[]>(reader.GetOrdinal("col_bytes")), Is.EqualTo(ValueOrNull(reader["col_bytes"])));
170+
Assert.That(reader.GetFieldValue<DateOnly?>(reader.GetOrdinal("col_date")), Is.EqualTo(ValueOrNull(reader["col_date"])));
171+
Assert.That(reader.GetFieldValue<float?>(reader.GetOrdinal("col_float32")), Is.EqualTo(ValueOrNull(reader["col_float32"])));
172+
Assert.That(reader.GetFieldValue<double?>(reader.GetOrdinal("col_float64")), Is.EqualTo(ValueOrNull(reader["col_float64"])));
173+
Assert.That(reader.GetFieldValue<long?>(reader.GetOrdinal("col_int64")), Is.EqualTo(ValueOrNull(reader["col_int64"])));
174+
Assert.That(reader.GetFieldValue<TimeSpan?>(reader.GetOrdinal("col_interval")), Is.EqualTo(ValueOrNull(reader["col_interval"])));
175+
Assert.That(reader.GetFieldValue<string?>(reader.GetOrdinal("col_json")), Is.EqualTo(ValueOrNull(reader["col_json"])));
176+
Assert.That(reader.GetFieldValue<decimal?>(reader.GetOrdinal("col_numeric")), Is.EqualTo(ValueOrNull(reader["col_numeric"])));
177+
Assert.That(reader.GetFieldValue<string?>(reader.GetOrdinal("col_string")), Is.EqualTo(ValueOrNull(reader["col_string"])));
178+
Assert.That(reader.GetFieldValue<DateTime?>(reader.GetOrdinal("col_timestamp")), Is.EqualTo(ValueOrNull(reader["col_timestamp"])));
179+
Assert.That(reader.GetFieldValue<Guid?>(reader.GetOrdinal("col_uuid")), Is.EqualTo(ValueOrNull(reader["col_uuid"])));
180+
181+
Assert.That(reader.GetFieldValue<List<bool?>>(reader.GetOrdinal("col_array_bool")), Is.EqualTo(ValueOrNull(reader["col_array_bool"])));
182+
Assert.That(reader.GetFieldValue<List<byte[]>>(reader.GetOrdinal("col_array_bytes")), Is.EqualTo(ValueOrNull(reader["col_array_bytes"])));
183+
Assert.That(reader.GetFieldValue<List<DateOnly?>>(reader.GetOrdinal("col_array_date")), Is.EqualTo(ValueOrNull(reader["col_array_date"])));
184+
Assert.That(reader.GetFieldValue<List<float?>>(reader.GetOrdinal("col_array_float32")), Is.EqualTo(ValueOrNull(reader["col_array_float32"])));
185+
Assert.That(reader.GetFieldValue<List<double?>>(reader.GetOrdinal("col_array_float64")), Is.EqualTo(ValueOrNull(reader["col_array_float64"])));
186+
Assert.That(reader.GetFieldValue<List<long?>>(reader.GetOrdinal("col_array_int64")), Is.EqualTo(ValueOrNull(reader["col_array_int64"])));
187+
Assert.That(reader.GetFieldValue<List<TimeSpan?>>(reader.GetOrdinal("col_array_interval")), Is.EqualTo(ValueOrNull(reader["col_array_interval"])));
188+
Assert.That(reader.GetFieldValue<List<string?>>(reader.GetOrdinal("col_array_json")), Is.EqualTo(ValueOrNull(reader["col_array_json"])));
189+
Assert.That(reader.GetFieldValue<List<decimal?>>(reader.GetOrdinal("col_array_numeric")), Is.EqualTo(ValueOrNull(reader["col_array_numeric"])));
190+
Assert.That(reader.GetFieldValue<List<string?>>(reader.GetOrdinal("col_array_string")), Is.EqualTo(ValueOrNull(reader["col_array_string"])));
191+
Assert.That(reader.GetFieldValue<List<DateTime?>>(reader.GetOrdinal("col_array_timestamp")), Is.EqualTo(ValueOrNull(reader["col_array_timestamp"])));
192+
Assert.That(reader.GetFieldValue<List<Guid?>>(reader.GetOrdinal("col_array_uuid")), Is.EqualTo(ValueOrNull(reader["col_array_uuid"])));
193+
}
194+
}
195+
196+
private static object? ValueOrNull(object value)
197+
{
198+
return value is DBNull ? null : value;
199+
}
145200

146201
[Test]
147202
public void TestExecuteDdl()

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

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,47 @@ namespace Google.Cloud.Spanner.DataProvider.Tests;
2222

2323
public class BatchTests : AbstractMockServerTests
2424
{
25-
[TestCase(1, false)]
26-
[TestCase(2, false)]
27-
[TestCase(5, false)]
28-
[TestCase(1, true)]
29-
[TestCase(2, true)]
30-
[TestCase(5, true)]
31-
public async Task TestAllParameterTypes(int numCommands, bool executeAsync)
25+
[TestCase(1, false, false)]
26+
[TestCase(2, false, false)]
27+
[TestCase(5, false, false)]
28+
[TestCase(1, true, false)]
29+
[TestCase(2, true, false)]
30+
[TestCase(5, true, false)]
31+
[TestCase(1, false, true)]
32+
[TestCase(2, false, true)]
33+
[TestCase(5, false, true)]
34+
[TestCase(1, true, true)]
35+
[TestCase(2, true, true)]
36+
[TestCase(5, true, true)]
37+
public async Task TestAllParameterTypes(int numCommands, bool executeAsync, bool useTransaction)
3238
{
3339
await using var connection = new SpannerConnection();
3440
connection.ConnectionString = ConnectionString;
3541
await connection.OpenAsync();
3642

37-
const string insert = "insert into my_table values (@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12)";
43+
SpannerTransaction? transaction = null;
44+
if (useTransaction)
45+
{
46+
if (executeAsync)
47+
{
48+
transaction = await connection.BeginTransactionAsync();
49+
}
50+
else
51+
{
52+
// ReSharper disable once MethodHasAsyncOverload
53+
transaction = connection.BeginTransaction();
54+
}
55+
}
56+
57+
const string insert = "insert into my_table values " +
58+
"(@p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11, @p12, @p13, @p14, @p15, @p16, @p17, @p18, @p19, @p20, @p21, @p22, @p23)";
3859
Fixture.SpannerMock.AddOrUpdateStatementResult(insert, StatementResult.CreateUpdateCount(1));
3960

4061
await using var batch = connection.CreateBatch();
62+
if (transaction != null)
63+
{
64+
batch.Transaction = transaction;
65+
}
4166

4267
for (var i = 0; i < numCommands; i++)
4368
{
@@ -90,6 +115,18 @@ public async Task TestAllParameterTypes(int numCommands, bool executeAsync)
90115
{
91116
Assert.That(command.RecordsAffected, Is.EqualTo(1));
92117
}
118+
if (transaction != null)
119+
{
120+
if (executeAsync)
121+
{
122+
await transaction.CommitAsync();
123+
}
124+
else
125+
{
126+
// ReSharper disable once MethodHasAsyncOverload
127+
transaction.Commit();
128+
}
129+
}
93130

94131
var requests = Fixture.SpannerMock.Requests.ToList();
95132
Assert.That(requests.OfType<ExecuteBatchDmlRequest>().Count, Is.EqualTo(1));
@@ -196,6 +233,8 @@ public async Task TestAllParameterTypes(int numCommands, bool executeAsync)
196233
Assert.That(fields["p23"].ListValue.Values[0].NumberValue, Is.EqualTo(3.14f));
197234
Assert.That(fields["p23"].ListValue.Values[1].HasNullValue, Is.True);
198235
}
236+
var commitRequest = requests.OfType<CommitRequest>().First();
237+
Assert.That(commitRequest, Is.Not.Null);
199238
}
200239

201240
[TestCase(true)]

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

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using System.Data.Common;
2020
using System.Globalization;
2121
using System.IO;
22+
using System.Runtime.CompilerServices;
2223
using System.Text;
2324
using System.Threading;
2425
using System.Threading.Tasks;
@@ -528,11 +529,21 @@ public override System.Type GetFieldType(int ordinal)
528529
return GetClrType(Metadata!.RowType.Fields[ordinal].Type);
529530
}
530531

532+
private static System.Type GetNullableClrType(Google.Cloud.Spanner.V1.Type type)
533+
{
534+
var clr = GetClrType(type);
535+
if (clr.IsValueType)
536+
{
537+
return typeof(Nullable<>).MakeGenericType(clr);
538+
}
539+
return clr;
540+
}
541+
531542
private static System.Type GetClrType(Google.Cloud.Spanner.V1.Type type)
532543
{
533544
return type.Code switch
534545
{
535-
TypeCode.Array => typeof(List<>).MakeGenericType(GetClrType(type.ArrayElementType)),
546+
TypeCode.Array => typeof(List<>).MakeGenericType(GetNullableClrType(type.ArrayElementType)),
536547
TypeCode.Bool => typeof(bool),
537548
TypeCode.Bytes => typeof(byte[]),
538549
TypeCode.Date => typeof(DateOnly),
@@ -708,6 +719,10 @@ public override T GetFieldValue<T>(int ordinal)
708719
CheckNotClosed();
709720
CheckValidPosition();
710721
CheckValidOrdinal(ordinal);
722+
if (typeof(T) == typeof(object))
723+
{
724+
return base.GetFieldValue<T>(ordinal);
725+
}
711726
if (typeof(T) == typeof(Stream))
712727
{
713728
CheckNotNull(ordinal);
@@ -775,6 +790,15 @@ public override T GetFieldValue<T>(int ordinal)
775790
return (T)(object)GetInt64(ordinal);
776791
}
777792

793+
if (IsDBNull(ordinal) && default(T) == null)
794+
{
795+
if (typeof(T) == typeof(DBNull))
796+
{
797+
return (T)(object)DBNull.Value;
798+
}
799+
return (T)(object)null;
800+
}
801+
778802
return base.GetFieldValue<T>(ordinal);
779803
}
780804

@@ -797,11 +821,20 @@ private static object GetUnderlyingValue(Google.Cloud.Spanner.V1.Type type, Valu
797821
switch (type.Code)
798822
{
799823
case TypeCode.Array:
800-
var listType = typeof(List<>).MakeGenericType(GetClrType(type.ArrayElementType));
824+
var listType = GetClrType(type);
801825
var list = (IList)Activator.CreateInstance(listType);
826+
if (list == null)
827+
{
828+
throw new InvalidOperationException($"Failed to create instance of type {listType}.");
829+
}
802830
foreach (var element in value.ListValue.Values)
803831
{
804-
list.Add(GetUnderlyingValue(type.ArrayElementType, element));
832+
var underlyingValue = GetUnderlyingValue(type.ArrayElementType, element);
833+
if (underlyingValue is DBNull)
834+
{
835+
underlyingValue = null;
836+
}
837+
list.Add(underlyingValue);
805838
}
806839
return list;
807840
case TypeCode.Bool:

spannerlib/wrappers/spannerlib-dotnet/spannerlib-dotnet-mockserver/RandomResultSetGenerator.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ public static StructType GenerateAllTypesRowType()
5757
};
5858
}
5959

60-
public static ResultSet Generate(int numRows)
60+
public static ResultSet Generate(int numRows, bool allowNull = false)
6161
{
62-
return Generate(GenerateAllTypesRowType(), numRows);
62+
return Generate(GenerateAllTypesRowType(), numRows, allowNull);
6363
}
6464

65-
public static ResultSet Generate(StructType rowType, int numRows)
65+
public static ResultSet Generate(StructType rowType, int numRows, bool allowNull = false)
6666
{
6767
var result = new ResultSet
6868
{
@@ -73,23 +73,27 @@ public static ResultSet Generate(StructType rowType, int numRows)
7373
};
7474
for (var i = 0; i < numRows; i++)
7575
{
76-
result.Rows.Add(GenerateRow(rowType));
76+
result.Rows.Add(GenerateRow(rowType, allowNull));
7777
}
7878
return result;
7979
}
8080

81-
private static ListValue GenerateRow(StructType rowType)
81+
private static ListValue GenerateRow(StructType rowType, bool allowNull)
8282
{
8383
var row = new ListValue();
8484
foreach (var field in rowType.Fields)
8585
{
86-
row.Values.Add(GenerateValue(field.Type));
86+
row.Values.Add(GenerateValue(field.Type, allowNull));
8787
}
8888
return row;
8989
}
9090

91-
private static Value GenerateValue(Spanner.V1.Type type)
91+
private static Value GenerateValue(Spanner.V1.Type type, bool allowNull)
9292
{
93+
if (allowNull && Random.Shared.Next(10) == 5)
94+
{
95+
return Value.ForNull();
96+
}
9397
switch (type.Code)
9498
{
9599
case TypeCode.Bool:
@@ -106,7 +110,7 @@ private static Value GenerateValue(Spanner.V1.Type type)
106110
case TypeCode.Float64:
107111
return Value.ForNumber(Random.Shared.NextDouble() * double.MaxValue);
108112
case TypeCode.Int64:
109-
return Value.ForNumber(Random.Shared.NextInt64());
113+
return Value.ForString(Random.Shared.NextInt64().ToString());
110114
case TypeCode.Interval:
111115
var timespan = TimeSpan.FromTicks(Random.Shared.NextInt64());
112116
return Value.ForString(XmlConvert.ToString(timespan));
@@ -129,7 +133,7 @@ private static Value GenerateValue(Spanner.V1.Type type)
129133
var values = new Value[length];
130134
for (var i = 0; i < length; i++)
131135
{
132-
values[i] = GenerateValue(type.ArrayElementType);
136+
values[i] = GenerateValue(type.ArrayElementType, allowNull);
133137
}
134138
return Value.ForList(values);
135139
default:

0 commit comments

Comments
 (0)