66using System . Data ;
77using System . Diagnostics ;
88using System . Globalization ;
9+ using System . Xml . Linq ;
910using Xunit ;
1011
1112namespace Microsoft . Data . SqlClient . ManualTesting . Tests
1213{
1314 public static class SqlStreamingXmlTest
1415 {
1516 [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
16- public static void LinearSingleNode ( )
17+ public static void Linear_SingleNode ( )
1718 {
18- SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
19- // Use a literal XML column of the specified size. The XML is constructed by replicating a string of 'B' characters to reach the desired size, and wrapping it in XML tags.
19+ // Use literal XML column constructed by replicating a string of 'B' characters to reach the desired size, and wrapping it in XML tags.
2020 const string commandTextBase = "SELECT Convert(xml, N'<foo>' + REPLICATE(CAST('' AS nvarchar(max)) +N'B', ({0} * 1024 * 1024) - 11) + N'</foo>')" ;
2121
2222 TimeSpan time1 = TimedExecution ( commandTextBase , 1 ) ;
@@ -27,10 +27,9 @@ public static void LinearSingleNode()
2727 }
2828
2929 [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
30- public static void LinearMultipleNodes ( )
30+ public static void Linear_MultipleNodes ( )
3131 {
32- SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
33- // Use a literal XML column with the specified number of 1MB elements. The XML is constructed by replicating a string of 'B' characters to reach 1MB, then replicating to the desired number of elements.
32+ // Use literal XML column constructed by replicating a string of 'B' characters to reach 1MB, then replicating to the desired number of elements.
3433 const string commandTextBase = "SELECT Convert(xml, REPLICATE(N'<foo>' + REPLICATE(CAST('' AS nvarchar(max)) + N'B', (1024 * 1024) - 11) + N'</foo>', {0}))" ;
3534
3635 TimeSpan time1 = TimedExecution ( commandTextBase , 1 ) ;
@@ -40,10 +39,244 @@ public static void LinearMultipleNodes()
4039 Assert . True ( time5 . TotalMilliseconds <= ( time1 . TotalMilliseconds * 6 ) , $ "Execution time did not follow linear scale: 1x={ time1 . TotalMilliseconds } ms vs. 5x={ time5 . TotalMilliseconds } ms") ;
4140 }
4241
42+ [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
43+ public static void GetChars_RequiresBuffer ( )
44+ {
45+ SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
46+ const string commandText = "SELECT Convert(xml, N'<foo>bar</foo>')" ;
47+ long charCount = 0 ;
48+
49+ using ( SqlCommand command = connection . CreateCommand ( ) )
50+ {
51+ connection . Open ( ) ;
52+ command . CommandText = commandText ;
53+
54+ SqlDataReader sqlDataReader = command . ExecuteReader ( CommandBehavior . SequentialAccess ) ;
55+ if ( sqlDataReader . Read ( ) )
56+ {
57+ charCount = sqlDataReader . GetChars ( 0 , 0 , null , 0 , 1 ) ;
58+ }
59+ connection . Close ( ) ;
60+ }
61+
62+ //verify -1 is returned since buffer was not provided
63+ Assert . Equal ( - 1 , charCount ) ;
64+ }
65+
66+ [ ConditionalTheory ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
67+ [ InlineData ( true ) ]
68+ [ InlineData ( false ) ]
69+ public static void GetChars_SequentialDataIndex ( bool backwards )
70+ {
71+ SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
72+ const string commandText = "SELECT Convert(xml, N'<foo>bar</foo>')" ;
73+ char [ ] buffer = new char [ 2 ] ;
74+
75+ using ( SqlCommand command = connection . CreateCommand ( ) )
76+ {
77+ connection . Open ( ) ;
78+ command . CommandText = commandText ;
79+
80+ SqlDataReader sqlDataReader = command . ExecuteReader ( CommandBehavior . SequentialAccess ) ;
81+ if ( sqlDataReader . Read ( ) )
82+ {
83+ sqlDataReader . GetChars ( 0 , 0 , buffer , 0 , 2 ) ;
84+ // Verify that providing the same or lower index than the previous call results in an exception.
85+ // When backwards is true we test providing an index that is one less than the previous call,
86+ // otherwise we test providing the same index as the previous call - both should not be allowed.
87+ Assert . Throws < InvalidOperationException > ( ( ) => sqlDataReader . GetChars ( 0 , backwards ? 0 : 1 , buffer , 0 , 2 ) ) ;
88+ }
89+ connection . Close ( ) ;
90+ }
91+ }
92+
93+ [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
94+ public static void GetChars_PartialSingleElement ( )
95+ {
96+ SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
97+ const string commandText = "SELECT Convert(xml, N'<foo>_bar_baz</foo>')" ;
98+ long charCount = 0 ;
99+ char [ ] buffer = new char [ 3 ] ;
100+
101+ using ( SqlCommand command = connection . CreateCommand ( ) )
102+ {
103+ connection . Open ( ) ;
104+ command . CommandText = commandText ;
105+
106+ SqlDataReader sqlDataReader = command . ExecuteReader ( CommandBehavior . SequentialAccess ) ;
107+ if ( sqlDataReader . Read ( ) )
108+ {
109+ // Read just the 'bar' characters from the XML by specifying the offset, and the length of 3.
110+ // The offset is 6 to skip the entire first element '<foo>' and the initial '_' part of text.
111+ charCount = sqlDataReader . GetChars ( 0 , 6 , buffer , 0 , 3 ) ;
112+ }
113+ connection . Close ( ) ;
114+ }
115+
116+ Assert . Equal ( 3 , charCount ) ;
117+ Assert . Equal ( "bar" , new string ( buffer ) ) ;
118+ }
119+
120+ [ ConditionalTheory ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
121+ [ InlineData ( true ) ]
122+ [ InlineData ( false ) ]
123+ public static void GetChars_PartialAcrossElements ( bool initialRead )
124+ {
125+ SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
126+ const string commandText = "SELECT Convert(xml, N'<foobar>baz</foobar>')" ;
127+ long charCount = 0 ;
128+ char [ ] buffer = new char [ 8 ] ;
129+
130+ using ( SqlCommand command = connection . CreateCommand ( ) )
131+ {
132+ connection . Open ( ) ;
133+ command . CommandText = commandText ;
134+
135+ SqlDataReader sqlDataReader = command . ExecuteReader ( CommandBehavior . SequentialAccess ) ;
136+ if ( sqlDataReader . Read ( ) )
137+ {
138+ if ( initialRead )
139+ {
140+ // When initialRead is true, we verify continuation after a previous read,
141+ // otherwise we just verify that we can read across XML elements in a single call.
142+ char [ ] initialBuffer = new char [ 2 ] ;
143+ sqlDataReader . GetChars ( 0 , 0 , initialBuffer , 0 , 2 ) ;
144+ Assert . Equal ( "<f" , new string ( initialBuffer ) ) ;
145+ // Verify skipping within the existing initial element.
146+ sqlDataReader . GetChars ( 0 , 3 , initialBuffer , 0 , 2 ) ;
147+ Assert . Equal ( "ob" , new string ( initialBuffer ) ) ;
148+ }
149+ // Read the 'r>baz</f' characters across XML elements by specifying the offset, and the length of 8.
150+ // The offset is 6 to skip the '<fooba' characters.
151+ charCount = sqlDataReader . GetChars ( 0 , 6 , buffer , 0 , 8 ) ;
152+ }
153+ connection . Close ( ) ;
154+ }
155+
156+ Assert . Equal ( 8 , charCount ) ;
157+ Assert . Equal ( "r>baz</f" , new string ( buffer ) ) ;
158+ }
159+
160+ [ ConditionalTheory ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
161+ [ InlineData ( true ) ]
162+ [ InlineData ( false ) ]
163+ public static void GetChars_ExcessiveLength ( bool initialRead )
164+ {
165+ SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
166+ string xml = """<foo>_bar_baz</foo>""" ;
167+ int expectedSize = xml . Length ;
168+ string commandText = $ "SELECT Convert(xml, N'{ xml } ')";
169+
170+ using ( SqlCommand command = connection . CreateCommand ( ) )
171+ {
172+ connection . Open ( ) ;
173+ command . CommandText = commandText ;
174+
175+ SqlDataReader sqlDataReader = command . ExecuteReader ( CommandBehavior . SequentialAccess ) ;
176+ if ( sqlDataReader . Read ( ) )
177+ {
178+ if ( initialRead )
179+ {
180+ // When initialRead is true, we verify continuation after a previous read,
181+ // otherwise we just verify that we can read everything in a single call.
182+ char [ ] initialBuffer = new char [ 2 ] ;
183+ long initialLength = sqlDataReader . GetChars ( 0 , 0 , initialBuffer , 0 , 2 ) ;
184+ char [ ] remainingBuffer = new char [ 98 ] ;
185+ long remainingLength = sqlDataReader . GetChars ( 0 , 2 , remainingBuffer , 0 , 98 ) ;
186+ string combined = new string ( initialBuffer ) + new string ( remainingBuffer ) ;
187+
188+ Assert . Equal ( expectedSize , initialLength + remainingLength ) ;
189+ Assert . Equal ( xml , combined . Substring ( 0 , expectedSize ) ) ;
190+ }
191+ else
192+ {
193+ // Try to read more characters than the actual XML to verify that the method returns only the actual number of characters.
194+ ( long length , string text ) = ReadAllChars ( sqlDataReader , 100 ) ;
195+
196+ Assert . Equal ( expectedSize , length ) ;
197+ Assert . Equal ( xml , text . Substring ( 0 , expectedSize ) ) ;
198+ }
199+ }
200+ connection . Close ( ) ;
201+ }
202+ }
203+
204+ [ ConditionalTheory ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
205+ [ InlineData ( true ) ]
206+ [ InlineData ( false ) ]
207+ public static void GetChars_ExcessiveDataIndex ( bool initialRead )
208+ {
209+ SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
210+ string xml = """<foo>_bar_baz</foo>""" ;
211+ string commandText = $ "SELECT Convert(xml, N'{ xml } ')";
212+
213+ using ( SqlCommand command = connection . CreateCommand ( ) )
214+ {
215+ connection . Open ( ) ;
216+ command . CommandText = commandText ;
217+
218+ SqlDataReader sqlDataReader = command . ExecuteReader ( CommandBehavior . SequentialAccess ) ;
219+ if ( sqlDataReader . Read ( ) )
220+ {
221+ if ( initialRead )
222+ {
223+ // When initialRead is true, we verify continuation after a previous read,
224+ // otherwise we just verify the large DataIndex in a single call.
225+ char [ ] initialBuffer = new char [ 2 ] ;
226+ long initialLength = sqlDataReader . GetChars ( 0 , 0 , initialBuffer , 0 , 2 ) ;
227+ Assert . Equal ( 2 , initialLength ) ;
228+ }
229+
230+ // buffer will not be touched since the DataIndex is beyond the end of the XML, but a suitable buffer must still be provided.
231+ char [ ] buffer = new char [ 100 ] ;
232+ long length = sqlDataReader . GetChars ( 0 , 100 , buffer , 0 , 2 ) ;
233+ Assert . Equal ( 0 , length ) ;
234+ }
235+ connection . Close ( ) ;
236+ }
237+ }
238+
239+ [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringsSetup ) ) ]
240+ public static void GetChars_AsXDocument ( )
241+ {
242+ SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
243+ // Use a more complex XML column verify through XDocument.
244+ string xml = """<Person Id="1" Role="Admin"><Name>John</Name><Children /><PreservedWhitespace xml:space="preserve"> </PreservedWhitespace></Person>""" ;
245+ XDocument expect = XDocument . Parse ( xml ) ;
246+ int expectedSize = xml . Length ;
247+ string commandText = $ "SELECT Convert(xml, N'{ xml } ')";
248+
249+ using ( SqlCommand command = connection . CreateCommand ( ) )
250+ {
251+ connection . Open ( ) ;
252+ command . CommandText = commandText ;
253+
254+ SqlDataReader sqlDataReader = command . ExecuteReader ( CommandBehavior . SequentialAccess ) ;
255+ if ( sqlDataReader . Read ( ) )
256+ {
257+ ( long length , string xmlString ) = ReadAllChars ( sqlDataReader , expectedSize ) ;
258+
259+ Assert . Equal ( expectedSize , length ) ;
260+ XDocument actual = XDocument . Parse ( xmlString ) ;
261+ Assert . Equal ( ( int ) expect . Root . Attribute ( "Id" ) , ( int ) actual . Root . Attribute ( "Id" ) ) ;
262+ Assert . Equal ( ( string ) expect . Root . Attribute ( "Role" ) , ( string ) actual . Root . Attribute ( "Role" ) ) ;
263+ Assert . NotNull ( expect . Root . Element ( "Name" ) ? . Value ) ;
264+ Assert . Equal ( expect . Root . Element ( "Name" ) ! . Value , actual . Root . Element ( "Name" ) ! . Value ) ;
265+ Assert . NotNull ( expect . Root . Element ( "Children" ) ? . HasElements ) ;
266+ Assert . Equal ( expect . Root . Element ( "Children" ) ! . HasElements , actual . Root . Element ( "Children" ) ? . HasElements ) ;
267+ Assert . NotNull ( expect . Root . Element ( "PreservedWhitespace" ) ? . Value ) ;
268+ Assert . Equal ( expect . Root . Element ( "PreservedWhitespace" ) ! . Value , actual . Root . Element ( "PreservedWhitespace" ) ! . Value ) ;
269+ }
270+ connection . Close ( ) ;
271+ }
272+ }
273+
43274 private static TimeSpan TimedExecution ( string commandTextBase , int scale )
44275 {
45276 SqlConnection connection = new ( DataTestUtility . TCPConnectionString ) ;
46- var stopwatch = new Stopwatch ( ) ;
277+ Stopwatch stopwatch = new Stopwatch ( ) ;
278+ int expectedSize = scale * 1024 * 1024 ;
279+
47280
48281 using ( SqlCommand command = connection . CreateCommand ( ) )
49282 {
@@ -54,8 +287,9 @@ private static TimeSpan TimedExecution(string commandTextBase, int scale)
54287 if ( sqlDataReader . Read ( ) )
55288 {
56289 stopwatch . Start ( ) ;
57- ReadAllChars ( sqlDataReader , scale ) ;
290+ ( long length , string _ ) = ReadAllChars ( sqlDataReader , expectedSize ) ;
58291 stopwatch . Stop ( ) ;
292+ Assert . Equal ( expectedSize , length ) ;
59293 }
60294 connection . Close ( ) ;
61295 }
@@ -66,11 +300,10 @@ private static TimeSpan TimedExecution(string commandTextBase, int scale)
66300 /// <summary>
67301 /// Replicate the reading approach used with issue #1877
68302 /// </summary>
69- private static void ReadAllChars ( SqlDataReader sqlDataReader , int expectedMB )
303+ private static ( long , string ) ReadAllChars ( SqlDataReader sqlDataReader , int expectedSize )
70304 {
71- var expectedSize = expectedMB * 1024 * 1024 ;
72- var text = new char [ expectedSize ] ;
73- var buffer = new char [ 1 ] ;
305+ char [ ] text = new char [ expectedSize ] ;
306+ char [ ] buffer = new char [ 1 ] ;
74307
75308 long position = 0 ;
76309 long numCharsRead ;
@@ -85,7 +318,7 @@ private static void ReadAllChars(SqlDataReader sqlDataReader, int expectedMB)
85318 }
86319 while ( numCharsRead > 0 ) ;
87320
88- Assert . Equal ( expectedSize , position ) ;
321+ return ( position , new string ( text ) ) ;
89322 }
90323 }
91324}
0 commit comments