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