Skip to content

Commit 6c2d8d0

Browse files
committed
test: add more tests
1 parent 6645b29 commit 6c2d8d0

25 files changed

+1864
-79
lines changed

.github/workflows/ado-net-tests.yml

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,46 @@ jobs:
1414
os: [ubuntu-latest, macos-latest, windows-latest]
1515
runs-on: ${{ matrix.os }}
1616
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
1719
- name: Install dotnet
1820
uses: actions/setup-dotnet@v4
1921
with:
2022
dotnet-version: ${{ matrix.dotnet-version }}
21-
- name: Checkout code
22-
uses: actions/checkout@v4
23+
- name: Install Go
24+
uses: actions/setup-go@v5
25+
with:
26+
go-version: '1.25'
27+
# Install compilers for cross-compiling between operating systems.
28+
- name: Install compilers
29+
run: |
30+
echo "$RUNNER_OS"
31+
if [ "$RUNNER_OS" == "Windows" ]; then
32+
echo "Windows does not yet support cross compiling"
33+
elif [ "$RUNNER_OS" == "macOS" ]; then
34+
brew tap SergioBenitez/osxct
35+
brew install x86_64-unknown-linux-gnu
36+
brew install mingw-w64
37+
else
38+
sudo apt-get update
39+
sudo apt install -y g++-mingw-w64-x86-64 gcc-mingw-w64-x86-64
40+
sudo apt-get install -y gcc-arm-linux-gnueabihf
41+
fi
42+
shell: bash
43+
- name: Build the .NET wrapper
44+
working-directory: spannerlib/wrappers/spannerlib-dotnet
45+
run: |
46+
echo "$RUNNER_OS"
47+
./build.sh
48+
shell: bash
49+
- name: Restore .NET wrapper dependencies
50+
run: dotnet restore
51+
working-directory: spannerlib/wrappers/spannerlib-dotnet
52+
shell: bash
53+
- name: Build .NET wrapper
54+
run: dotnet build --no-restore -c Release
55+
working-directory: spannerlib/wrappers/spannerlib-dotnet
56+
shell: bash
2357
- name: spanner-ado-net-tests
2458
working-directory: drivers/spanner-ado-net/spanner-ado-net-tests
2559
run: dotnet test --verbosity normal

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

Lines changed: 29 additions & 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.Common;
1516
using Google.Cloud.SpannerLib.MockServer;
1617

1718
namespace Google.Cloud.Spanner.DataProvider.Tests;
@@ -28,17 +29,22 @@ static AbstractMockServerTests()
2829

2930
protected SpannerMockServerFixture Fixture;
3031

32+
protected SpannerDataSource DataSource { get; private set; }
33+
34+
3135
protected string ConnectionString => $"Host={Fixture.Host};Port={Fixture.Port};Data Source=projects/p1/instances/i1/databases/d1;UsePlainText=true";
3236

3337
[OneTimeSetUp]
3438
public void Setup()
3539
{
3640
Fixture = new SpannerMockServerFixture();
41+
DataSource = SpannerDataSource.Create(ConnectionString);
3742
}
3843

3944
[OneTimeTearDown]
4045
public void Teardown()
4146
{
47+
DataSource.Dispose();
4248
Fixture.Dispose();
4349
}
4450

@@ -108,3 +114,26 @@ public static async Task<int> ExecuteNonQueryAsync(
108114
return await command.ExecuteScalarAsync(cancellationToken);
109115
}
110116
}
117+
118+
public static class SpannerCommandExtensions
119+
{
120+
internal static void AddParameter(this SpannerCommand command, string name, object? value)
121+
{
122+
var parameter = command.CreateParameter();
123+
parameter.ParameterName = name;
124+
parameter.Value = value;
125+
command.Parameters.Add(parameter);
126+
}
127+
}
128+
129+
public static class BatchExtensions
130+
{
131+
internal static void AddSpannerBatchCommand(this DbBatch batch, string sql)
132+
{
133+
var command = new SpannerBatchCommand
134+
{
135+
CommandText = sql
136+
};
137+
batch.BatchCommands.Add(command);
138+
}
139+
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
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+
using Google.Cloud.Spanner.Admin.Database.V1;
17+
using Google.Cloud.Spanner.V1;
18+
using Google.Cloud.SpannerLib.MockServer;
19+
using TypeCode = Google.Cloud.Spanner.V1.TypeCode;
20+
21+
namespace Google.Cloud.Spanner.DataProvider.Tests;
22+
23+
public class CommandParameterTests : AbstractMockServerTests
24+
{
25+
[Test]
26+
[TestCase(CommandBehavior.Default)]
27+
[TestCase(CommandBehavior.SequentialAccess)]
28+
public async Task InputAndOutputParameters(CommandBehavior behavior)
29+
{
30+
const string sql = "SELECT @c-1 AS c, @a+2 AS b";
31+
Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet(
32+
new List<Tuple<TypeCode, string>>([
33+
Tuple.Create(TypeCode.Int64, "c"),
34+
Tuple.Create(TypeCode.Int64, "b"),
35+
]),
36+
new List<object[]>([[3, 5]])));
37+
38+
await using var conn = await OpenConnectionAsync();
39+
await using var cmd = new SpannerCommand(sql, conn);
40+
cmd.AddParameter("a", 3);
41+
var b = new SpannerParameter { ParameterName = "b", Direction = ParameterDirection.Output };
42+
cmd.Parameters.Add(b);
43+
var c = new SpannerParameter { ParameterName = "c", Direction = ParameterDirection.InputOutput, Value = 4 };
44+
cmd.Parameters.Add(c);
45+
await using (await cmd.ExecuteReaderAsync(behavior))
46+
{
47+
// TODO: Enable if we decide to support output parameters in the same way as npgsql.
48+
// Assert.That(b.Value, Is.EqualTo(5));
49+
// Assert.That(c.Value, Is.EqualTo(3));
50+
}
51+
var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest;
52+
Assert.That(request, Is.Not.Null);
53+
Assert.That(request.Params.Fields.Count, Is.EqualTo(3));
54+
Assert.That(request.Params.Fields["a"].StringValue, Is.EqualTo("3"));
55+
Assert.That(request.Params.Fields["b"].HasNullValue);
56+
Assert.That(request.Params.Fields["c"].StringValue, Is.EqualTo("4"));
57+
}
58+
59+
[Test]
60+
public async Task SendWithoutType([Values(PrepareOrNot.NotPrepared, PrepareOrNot.Prepared)] PrepareOrNot prepare)
61+
{
62+
const string sql = "select cast(@p as timestamp)";
63+
Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(
64+
new V1.Type{Code = TypeCode.Timestamp}, "p", "2025-10-30T10:00:00.000000000Z"));
65+
66+
await using var conn = await OpenConnectionAsync();
67+
await using var cmd = new SpannerCommand(sql, conn);
68+
cmd.AddParameter("p", "2025-10-30T10:00:00Z");
69+
if (prepare == PrepareOrNot.Prepared)
70+
{
71+
await cmd.PrepareAsync();
72+
}
73+
74+
await using var reader = await cmd.ExecuteReaderAsync();
75+
await reader.ReadAsync();
76+
Assert.That(reader.GetValue(0), Is.EqualTo(new DateTime(2025, 10, 30, 10, 0, 0, DateTimeKind.Utc)));
77+
78+
var request = Fixture.SpannerMock.Requests.First(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest;
79+
Assert.That(request, Is.Not.Null);
80+
Assert.That(request.Params.Fields.Count, Is.EqualTo(1));
81+
Assert.That(request.Params.Fields["p"].StringValue, Is.EqualTo("2025-10-30T10:00:00Z"));
82+
Assert.That(request.ParamTypes.Count, Is.EqualTo(0));
83+
84+
var expectedCount = prepare == PrepareOrNot.Prepared ? 2 : 1;
85+
Assert.That(Fixture.SpannerMock.Requests.Count(r => r is ExecuteSqlRequest { Sql: sql }), Is.EqualTo(expectedCount));
86+
}
87+
88+
[Test]
89+
public async Task PositionalParameter()
90+
{
91+
// Set the database dialect to PostgreSQL to enable the use of PostgreSQL-style positional parameters.
92+
Fixture.SpannerMock.AddDialectResult(DatabaseDialect.Postgresql);
93+
const string sql = "SELECT $1";
94+
Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateSingleColumnResultSet(
95+
new V1.Type{Code = TypeCode.Int64}, "c", 8L));
96+
97+
await using var conn = await OpenConnectionAsync();
98+
await using var cmd = new SpannerCommand(sql, conn);
99+
cmd.Parameters.Add(new SpannerParameter { Value = 8 });
100+
Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(8));
101+
102+
var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest;
103+
Assert.That(request, Is.Not.Null);
104+
Assert.That(request.Params.Fields.Count, Is.EqualTo(1));
105+
Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("8"));
106+
Assert.That(request.ParamTypes.Count, Is.EqualTo(0));
107+
}
108+
109+
[Test]
110+
public async Task UnreferencedNamedParameterIsIgnored()
111+
{
112+
await using var conn = await OpenConnectionAsync();
113+
await using var cmd = new SpannerCommand("SELECT 1", conn);
114+
cmd.AddParameter("not_used", 8);
115+
Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(1));
116+
117+
var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: "SELECT 1" }) as ExecuteSqlRequest;
118+
Assert.That(request, Is.Not.Null);
119+
Assert.That(request.Params.Fields.Count, Is.EqualTo(1));
120+
Assert.That(request.Params.Fields["not_used"].StringValue, Is.EqualTo("8"));
121+
Assert.That(request.ParamTypes.Count, Is.EqualTo(0));
122+
}
123+
124+
[Test]
125+
public async Task UnreferencedPositionalParameterIsIgnored()
126+
{
127+
// Set the database dialect to PostgreSQL to enable the use of PostgreSQL-style positional parameters.
128+
Fixture.SpannerMock.AddDialectResult(DatabaseDialect.Postgresql);
129+
await using var conn = await OpenConnectionAsync();
130+
await using var cmd = new SpannerCommand("SELECT 1", conn);
131+
cmd.Parameters.Add(new SpannerParameter { Value = 8 });
132+
Assert.That(await cmd.ExecuteScalarAsync(), Is.EqualTo(1));
133+
134+
var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: "SELECT 1" }) as ExecuteSqlRequest;
135+
Assert.That(request, Is.Not.Null);
136+
Assert.That(request.Params.Fields.Count, Is.EqualTo(1));
137+
Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("8"));
138+
Assert.That(request.ParamTypes.Count, Is.EqualTo(0));
139+
}
140+
141+
[Test]
142+
public void ParameterName()
143+
{
144+
var command = new SpannerCommand();
145+
146+
// Add parameters.
147+
command.Parameters.Add(new SpannerParameter{ ParameterName = "@Parameter1", DbType = DbType.Boolean, Value = true });
148+
command.Parameters.Add(new SpannerParameter{ ParameterName = "@Parameter2", DbType = DbType.Int32, Value = 1 });
149+
command.Parameters.Add(new SpannerParameter{ ParameterName = "Parameter3", DbType = DbType.DateTime, Value = DBNull.Value });
150+
command.Parameters.Add(new SpannerParameter{ ParameterName = "Parameter4", DbType = DbType.Binary, Value = DBNull.Value });
151+
152+
var parameter = command.Parameters["@Parameter1"];
153+
Assert.That(parameter, Is.Not.Null);
154+
command.Parameters[0].Value = 1;
155+
156+
Assert.That(command.Parameters["@Parameter1"].ParameterName, Is.EqualTo("@Parameter1"));
157+
Assert.That(command.Parameters["@Parameter2"].ParameterName, Is.EqualTo("@Parameter2"));
158+
Assert.That(command.Parameters["Parameter3"].ParameterName, Is.EqualTo("Parameter3"));
159+
Assert.That(command.Parameters["Parameter4"].ParameterName, Is.EqualTo("Parameter4"));
160+
161+
Assert.That(command.Parameters[0].ParameterName, Is.EqualTo("@Parameter1"));
162+
Assert.That(command.Parameters[1].ParameterName, Is.EqualTo("@Parameter2"));
163+
Assert.That(command.Parameters[2].ParameterName, Is.EqualTo("Parameter3"));
164+
Assert.That(command.Parameters[3].ParameterName, Is.EqualTo("Parameter4"));
165+
166+
// Verify that the '@' is stripped before being sent to Spanner.
167+
var statement = command.BuildStatement();
168+
Assert.That(statement, Is.Not.Null);
169+
Assert.That(statement.Params.Fields.Count, Is.EqualTo(4));
170+
Assert.That(statement.Params.Fields["Parameter1"].StringValue, Is.EqualTo("1"));
171+
Assert.That(statement.Params.Fields["Parameter2"].StringValue, Is.EqualTo("1"));
172+
Assert.That(statement.Params.Fields["Parameter3"].HasNullValue);
173+
Assert.That(statement.Params.Fields["Parameter4"].HasNullValue);
174+
175+
Assert.That(statement.ParamTypes.Count, Is.EqualTo(4));
176+
Assert.That(statement.ParamTypes["Parameter1"].Code, Is.EqualTo(TypeCode.Bool));
177+
Assert.That(statement.ParamTypes["Parameter2"].Code, Is.EqualTo(TypeCode.Int64));
178+
Assert.That(statement.ParamTypes["Parameter3"].Code, Is.EqualTo(TypeCode.Timestamp));
179+
Assert.That(statement.ParamTypes["Parameter4"].Code, Is.EqualTo(TypeCode.Bytes));
180+
}
181+
182+
[Test]
183+
public async Task SameParamMultipleTimes()
184+
{
185+
const string sql = "SELECT @p1, @p1";
186+
Fixture.SpannerMock.AddOrUpdateStatementResult(sql, StatementResult.CreateResultSet(
187+
new List<Tuple<TypeCode, string>>([Tuple.Create(TypeCode.Int64, "p1"), Tuple.Create(TypeCode.Int64, "p1")]),
188+
new List<object[]>([[8, 8]])));
189+
190+
await using var conn = await OpenConnectionAsync();
191+
await using var cmd = new SpannerCommand(sql, conn);
192+
cmd.AddParameter("@p1", 8);
193+
await using var reader = await cmd.ExecuteReaderAsync();
194+
await reader.ReadAsync();
195+
Assert.That(reader[0], Is.EqualTo(8));
196+
Assert.That(reader[1], Is.EqualTo(8));
197+
198+
var request = Fixture.SpannerMock.Requests.Single(r => r is ExecuteSqlRequest { Sql: sql }) as ExecuteSqlRequest;
199+
Assert.That(request, Is.Not.Null);
200+
Assert.That(request.Params.Fields.Count, Is.EqualTo(1));
201+
Assert.That(request.Params.Fields["p1"].StringValue, Is.EqualTo("8"));
202+
Assert.That(request.ParamTypes.Count, Is.EqualTo(0));
203+
}
204+
205+
[Test]
206+
public async Task ParameterMustBeSet()
207+
{
208+
await using var conn = await OpenConnectionAsync();
209+
await using var cmd = new SpannerCommand("SELECT @p1::TEXT", conn);
210+
cmd.Parameters.Add(new SpannerParameter{ ParameterName = "@p1" });
211+
212+
Assert.That(async () => await cmd.ExecuteReaderAsync(),
213+
Throws.Exception
214+
.TypeOf<InvalidOperationException>()
215+
.With.Message.EqualTo("Parameter @p1 has no value"));
216+
}
217+
218+
}

0 commit comments

Comments
 (0)